這是一個典型的演算法題目,它看似簡單,但藏著對效能要求的陷阱。直接按照題意模擬會超時,需要我們找出問題的內在規律,設計更聰明的演算法。
首先,我們重新檢視選擇廁所的三條規則:
min(i-Li, Ri-i)
最大者。max(i-Li, Ri-i)
最大者。i
最小者。這三條規則聽起來有點複雜,但我們可以把它們簡化。想像一下,一個空廁所區間由左右兩個已佔用的人 L
和 R
界定。
將這三條規則結合起來,我們可以得到一個非常清晰的核心策略:
在任何時候,下一個人永遠會選擇當下「最長的連續空廁所區間」,並坐到該區間的正中央。如果存在多個長度相同的最長區間,他會選擇最靠左的那一個。
最直觀的想法就是完全按照上面的核心策略來模擬。
PriorityQueue
)來儲存所有「空的區間」。PriorityQueue
的排序規則就是我們的核心策略:優先按區間長度由大到小排序;如果長度相同,則按起始位置由小到大排序。k
個人。在每次迴圈中:
PriorityQueue
取出最優的區間。PriorityQueue
中。這個方法哪裡有問題?
答案是效能。題目給的限制是 n
和 k
最大可達 1018。一個一個模擬 k
次的迴圈,當 k
是天文數字時,程式絕對會跑到天荒地老,導致超時 (Time Limit Exceeded)。
因此,我們需要一種方法,能夠「快轉」這個模擬過程。
既然不能一個一個模擬,我們可以一次「跳過」一大批人。關鍵點在於,所有長度相同的最長區間,在被更短的區間考慮之前,一定會被優先填滿。
這啟發了我們的「計數與跳躍」 (Count and Skip) 策略:
第一階段:快速跳躍,確定目標區間的屬性
我們不需要知道每個區間的具體位置,只需要追蹤**「不同長度的區間各有多少個」**。
資料結構:使用 TreeMap<長度, 數量>
。TreeMap
可以自動幫我們把「長度」這個鍵 (key) 排序,我們設定它由大到小排序,這樣每次都能輕易拿到最長區間的資訊。
模擬迴圈:
n
。我們剩下一個長度為 n-1
的大區間 (1, n)
。TreeMap
裡是 { n-1: 1 }
。我們要處理剩下 k-2
個人。TreeMap
中取出最長的區間長度 L
和其數量 C
。k-2 <= C
:太好了!這表示我們要找的第 k
個人,就是這 C
個長度為 L
的區間中,從左到右數過來的第 k-2
個。我們的目標已經鎖定,跳躍階段結束。k-2 > C
:表示這 C
個長度為 L
的區間會全部被填滿,但還輪不到我們要找的人。我們就「跳過」這 C
個人,讓 k-2 -= C
。C
個長度為 L
的區間分裂成子區間。每個長度 L
的區間會分裂成兩個長度為 L/2
和 L - L/2
的子區間。我們將這些新的子區間數量更新回 TreeMap
中,繼續下一輪迴圈。經過這個階段,我們會得到兩個重要資訊:
現在我們知道目標區間的「長相」,但不知道它在「哪裡」。這時就需要第二階段:從頭開始,遞迴地尋找這個特定區間的位置。
我們設計一個函式 findLocation(l, r, target_len, rank)
,它的任務是在 (l, r)
這個範圍內,找到長度為 target_len
且順位為 rank
的區間。
遞迴邏輯:
(l, r)
區間,我們先把它分裂成左、右兩個子區間 (l, mid)
和 (mid, r)
。(l, mid)
總共會產生多少個符合 target_len
的區間 (這裡需要另一個輔助的遞迴函式 countIn
來計算,並且用快取/記憶化來加速)。rank
小於等於左邊的計數,表示我們的目標在左子區間裡,我們就遞迴進入 findLocation(l, mid, ...)
。rank
大於左邊的計數,表示目標在右子區間裡。我們遞迴進入 findLocation(mid, r, ...)
,並且把 rank
減去左邊的計數 (rank -= count_from_left
)。遞迴終點:當 findLocation
的 (l, r)
區間長度剛好等於 target_len
時,表示我們已經找到了這個區間!直接計算它的中點 l + (r-l)/2
就是最終答案。
這個解法結合了多種演算法思想:
findLocation
和 countIn
函式都使用了遞迴,將大問題分解為小問題來求解。cache
來儲存 countIn
的計算結果,避免對相同區間進行重複的昂貴計算。透過這樣的層層分析與優化,我們就能寫出一個既正確又高效的程式,來解決這個看似棘手的問題。