遇到要求最大(最小值)時,每次決策都選擇當下最佳的選擇,這就是貪婪演算 法。然而一般的情況下,貪婪演算法不保證正確性,因此什麼時候可以 greedy、要怎麼 greedy,都需要一定的練習才可以熟練。
6.1 貪婪失敗的實例
以 0/1 背包問題為例。考慮以下三種貪婪的準則:
1. 每次都將重量最小的物品塞進包包。複雜度 O(n log n)。
2. 每次都將價值最高的物品塞進包包。複雜度 O(n log n)。
3. 每次都將 CP 值(價值除以重量)最高的物品塞進包包。複雜度 O(n log n)。
可以發現三者的複雜度都比前面的 DP 快許多,然而問題就在於三者都不保證正確 性。有興趣的人不妨試著對三種準則分別構造反例。
從這個例子,可以發現雖然 greedy 可以有低時間複雜度,卻不一定正確。不過好的 greedy 演算法會給出跟正解足夠接近的解。而在某些問題裡,greedy 更是會保證正確性。
6.2 貪婪成功的實例
我們考慮下列的問題:給你 n 個線段。請問最多能取出幾個線段,內部互不重疊。我 們再考慮以下三種貪婪的準則:
1. 將線段依線段長度排序,再由小到大選取。如果有重疊就不選,沒有重疊就選。複雜 度 O(n log n)。
2. 將線段依左端點排序,從最左邊的線段開始取。如果有重疊就不取,沒有重疊就取。
複雜度 O(n log n)。
3. 將線段依右端點排序,從最左邊的線段開始取。如果有重疊就不取,沒有重疊就取。
複雜度 O(n log n)。
XXIII
對於第一個準則,考慮線段 [0,5][4,6][5,10] 就發現這準則不一定正確。
對於第二個準則,考慮線段 [0,3][1,2][2,3] 就發現這準則也不一定正確。
然而可以證明第三個準則保證正確(請讀者自行思考),這是一個 greedy 成功的案例。
這個例子告訴我們就算可以 greedy,也要選擇一種好的方法,不能胡亂地貪婪。
6.3 習題
當你 greedy 失敗的時候,要仔細思考:為什麼這樣 greedy 會失敗?我該怎麼樣改變 我的準則才可以將剛剛的反例克服?當然,也要適時放棄 greedy 的想法。
1. (No judge) 有 n 位病人要看醫生。已知每個病人看醫生需要花的時間,而且只有一位醫 生。請求出病人總等待時間的最小值。時間複雜度 O(n log n)。
2. (Codeforces 665C) 給你一個長度≤ 2 · 105的字串。請用最少的 edit distance(改變最少個 字元)使得相鄰兩個字元皆相異。
3. (Codeforces 701A) 給你 n≤ 100 個數。請將這 n 個數兩兩分組使得每組和相同。保證做 得到。
4. (ZJ b231)(2009 入營考 pC) 有 N ≤ 1000 本書(事實上 N ≤ 105都可以),每本書都有所 需要的印刷時間和裝訂時間。你可以同時裝訂任意多本書,但同一時間只能印刷至多 一本書。每本書需要先印完再裝訂。請問你至少要花多久才能將所有書都印刷裝訂完 畢。
7 二分搜
7.1 一般的二分搜
通常一般的二分搜是在解決以下這種問題:如果有一個遞增的函數 f 定義在區間 [a, a + n) 上,請求出滿足 f (s)≥ c 的最小整數 s。
如果你從 a 開始暴搜,直到找到一個滿足條件的 s,那麼複雜度是 O(n)。這時我 們可以使用二分搜來解決這樣的問題,優化時間複雜度。想法是對於某個在 (a, a + n) 中 的整數 k,如果 f(k− 1) ≥ c,那麼 s < k,也就是說你要求的答案會落在區間 [a, k) 中。
反之,如果 f(k− 1) < c,那麼 s ≥ k,也就是說你要求的答案會落在 [k, a + n)。為了讓兩 種情況的可能性都儘量低,你可以發現你應該要取 k 愈接近 a + n/2 愈好。如此一來,每 次候選區間的長度都會縮小一半,因此複雜度為 O(log n)。
實務上,這種函數 f 常常不能直接得出某一點的值 f(a)(甚至只能確認它和 c 的大小 關係),而需要 O(M) 的時間來計算。顯然地,這時複雜度是 O(M log n)。
XXIV
順帶一提,lower_bound 和 upper_bound 便是用二分搜實作的。
實作上要注意的是加一和減一不要搞混、左閉右開和閉區間不要搞混,不然很有可能 就變成無窮迴圈。以下是虛擬碼:
Algorithm 5: Binary Search
1 function binary_search(array[], first, last, val) 2 while first + 1 < last
3 mid = (first + last) / 2 4 if (array[mid - 1] < val)
5 first = mid
6 else
7 last = mid
8 end if
9 end while
10 return first
11 end function
7.2 題外話:三分搜
利用二分搜這種「縮短候選人長度」的想法,我們可以找出滿足特定性質的函數的最 小值,這種技巧稱為三分搜。三分搜處理的問題如下:有一個在 [a, a + n) 中先嚴格遞減 再嚴格遞增的函數 f,請求出 f 在 [a, a + n) 的最小值。
取 在 [a, a + n) 中的 兩個 整 數 x < y。如 果 f(x) < f(y),那 麼最 小 值一 定落 在 [a, y)。如果 f (x) > f (y),那麼最小值一定落在 (x, a + n)。如果 f (x) = f (y),那麼最小值 一定落在 (x, y)。為了讓候選區間每次都會縮短一定的比例,通常都取 x 跟 y 為區間的三 等分點(取中間一點的話常數會變小)。複雜度仍然是 O(log n)。
7.3 對答案二分搜
有許多問題都喜歡叫你求「滿足條件的最小值」這種東西。如果這個問題滿足「單調 性」,那或許可以考慮對答案二分搜。
什麼是「單調性」呢?考慮一個函數 P ,如果 s 滿足條件,那麼 P (s) = 1,反之 則為 0。如果 P 有單調性,我們就說這個問題有單調性。這樣的好處是,我們可以直接用 前面的方法二分搜出要求的 s。如果計算 P 的複雜度並不大時,這樣的方法可以有非常好 的表現效率。在你沒辦法快速求出 s 而只能快速確認一個 s 是否符合條件時,這是一個非 常好的方法。
XXV
7.4 習題
1. (TIOJ 1839)(IOI 2013)(Interactive) 有 n≤ 5000 個開關,分別(不照順序地)連著 n 個門。
對於每個開關,要嘛開的時候門會開,要嘛關的時候門會開,反之則反。你最多可以 詢問 70000 次,對於每次詢問,你可以給一個 n 個開關的配置,程式會告訴你第一個 關著的門是幾號門。請找出開關和門之間的對應關係,以及會讓所有門都開的開關配 置。
2. (TIOJ 1341)(IOI 2007)(Interactive) 有圖,詳見題敘。
(提示:一開始將候選區間不斷倍增,直到確定所求答案位於候選區間內。有人稱這 種方法為倍增法。)
3. (TIOJ 1815)(IOI 2013) 你有 T ≤ 106 個玩具、A ≤ 5 · 104 個弱雞機器人和 B ≤ 5 · 104 個 小不點機器人。每個玩具的重量為 Wi、大小為 Si。每個弱雞機器人每次可以拿起的 玩具重量不能超過 Xi(大小不限制),而小不點機器人每次可以拿起的玩具大小不能 超過 Yi(重量不限制)。兩種機器人一次都只能拿一種玩具,每拿起一個玩具並放在 好好的地方需要花一分鐘,但不同的機器人可以同時拿玩具。請問你至少需要用幾分 鐘才能收拾完所有玩具(或不可能收完)?
(提示:greedy。)
XXVI