這是一個典型的演算法題目,它看似簡單,但藏著對效能要求的陷阱。直接按照題意模擬會超時,需要我們找出問題的內在規律,設計更聰明的演算法。
首先,我們重新檢視選擇廁所的三條規則:
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 的計算結果,避免對相同區間進行重複的昂貴計算。透過這樣的層層分析與優化,我們就能寫出一個既正確又高效的程式,來解決這個看似棘手的問題。