调度考生的座位
調度考生的座位
文章目錄
- 調度考生的座位
- 一、問題描述
- 二、分析
- 三、代碼
- 四、進階問題
- 五、最后總結
一、問題描述
先來描述一下題目:假設有一個考場,考場有一排共 N 個座位,索引分別是 [0..N-1],考生會陸續進入考場考試,并且可能在任何時候離開考場。
你作為考官,要安排考生們的座位,滿足:每當一個學生進入時,你需要最大化他和最近其他人的距離;如果有多個這樣的座位,安排到他到索引最小的那個座位。這很符合實際情況對吧,
也就是請你實現下面這樣一個類:
class ExamRoom {// 構造函數,傳入座位總數 Npublic ExamRoom(int N);// 來了一名考生,返回你給他分配的座位public int seat();// 坐在 p 位置的考生離開了// 可以認為 p 位置一定坐有考生public void leave(int p); }- 比方說考場有 5 個座位,分別是 [0…4]:
- 第一名考生進入時(調用 seat()),坐在任何位置都行,但是要給他安排索引最小的位置,也就是返回位置 0。
- 第二名學生進入時(再調用 seat()),要和旁邊的人距離最遠,也就是返回位置 4。
- 第三名學生進入時,要和旁邊的人距離最遠,應該做到中間,也就是座位 2。
- 如果再進一名學生,他可以坐在座位 1 或者 3,取較小的索引 1。
- 以此類推。
- 剛才所說的情況,沒有調用 leave 函數,不過讀者肯定能夠發現規律:
- 如果將每兩個相鄰的考生看做線段的兩端點,新安排考生就是找最長的線段,然后讓該考生在中間把這個線段「二分」,中點就是給他分配的座位。leave(p) 其實就是去除端點 p,使得相鄰兩個線段合并為一個。
- 核心思路很簡單對吧,所以這個問題實際上實在考察你對數據結構的理解。對于上述這個邏輯,你用什么數據結構來實現呢?
二、分析
- 根據上述思路,首先需要把坐在教室的學生抽象成線段,我們可以簡單的用一個大小為 2 的數組表示。
- 另外,思路需要我們找到「最長」的線段,還需要去除線段,增加線段。
- 但凡遇到在動態過程中取最值的要求,肯定要使用有序數據結構,我們常用的數據結構就是二叉堆和平衡二叉搜索樹了。
- 二叉堆實現的優先級隊列取最值的時間復雜度是 O(logN),但是只能刪除最大值。平衡二叉樹也可以取最值,也可以修改、刪除任意一個值,而且時間復雜度都是 O(logN)。
- 綜上,二叉堆不能滿足 leave 操作,應該使用平衡二叉樹。所以這里我們會用到 一種數據結構 TreeSet,這是一種有序數據結構,底層由紅黑樹維護有序性。
首先,如果有多個可選座位,需要選擇索引最小的座位對吧?我們先簡化一下問題,暫時不管這個要求,實現上述思路。
- 這個問題還用到一個常用的編程技巧,就是使用一個「虛擬線段」讓算法正確啟動,這就和鏈表相關的算法需要「虛擬頭結點」一個道理。
「虛擬線段」其實就是為了將所有座位表示為一個線段:
三、代碼
有了上述鋪墊,主要 API seat 和 leave 就可以寫了:
public int seat() {// 從有序集合拿出最長的線段int[] longest = pq.last();int x = longest[0];int y = longest[1];int seat;if (x == -1) { // 情況一seat = 0;} else if (y == N) { // 情況二seat = N - 1;} else { // 情況三seat = (y - x) / 2 + x;}// 將最長的線段分成兩段int[] left = new int[] {x, seat};int[] right = new int[] {seat, y};removeInterval(longest);addInterval(left);addInterval(right);return seat; }public void leave(int p) {// 將 p 左右的線段找出來int[] right = startMap.get(p);int[] left = endMap.get(p);// 合并兩個線段成為一個線段int[] merged = new int[] {left[0], right[1]};removeInterval(left);removeInterval(right);addInterval(merged); }- 至此,算法就基本實現了,代碼雖多,但思路很簡單:找最長的線段,從中間分隔成兩段,中點就是 seat() 的返回值;找 p 的左右線段,合并成一個線段,這就是 leave(p) 的邏輯
四、進階問題
但是,題目要求多個選擇時選擇索引最小的那個座位,我們剛才忽略了這個問題。比如下面這種情況會出錯:
- 現在有序集合里有線段 [0,4] 和 [4,9],那么最長線段 longest 就是后者,按照 seat 的邏輯,就會分割 [4,9],也就是返回座位 6。但正確答案應該是座位 2,因為 2 和 6 都滿足最大化相鄰考生距離的條件,二者應該取較小的。
- 遇到題目的這種要求,解決方式就是修改有序數據結構的排序方式。具體到這個問題,就是修改 TreeMap 的比較函數邏輯:
- 除此之外,還要改變 distance 函數,不能簡單地讓它計算一個線段兩個端點間的長度,而是讓它計算該線段中點和端點之間的長度。
- 這樣,[0,4] 和 [4,9] 的 distance 值就相等了,算法會比較二者的索引,取較小的線段進行分割。到這里,這道算法題目算是完全解決了。
五、最后總結
- 這個問題其實并不算難,雖然看起來代碼很多。核心問題就是考察有序數據結構的理解和使用。
- 處理動態問題一般都會用到有序數據結構,比如平衡二叉搜索樹和二叉堆,二者的時間復雜度差不多,但前者支持的操作更多。
- 既然平衡二叉搜索樹這么好用,還用二叉堆干嘛呢?因為二叉堆底層就是數組,實現簡單啊。
- 你實現個紅黑樹試試?操作復雜,而且消耗的空間相對來說會多一些。具體問題,還是要選擇恰當的數據結構來解決。
總結
- 上一篇: 最长回文子串和回文链表
- 下一篇: 二叉堆详解实现优先级队列