Chapter II. 新手上路
貪心演算法
貪心法 I
作者baluteshih

何謂貪心法?


若要用最簡單的方式去初步理解貪心法,其實就是

就好比現在有好多種蛋糕可以吃,但你的食量有限,最多也只能吃四、五塊蛋糕,那你要吃哪幾種蛋糕呢?當然是挑最好吃的那四、五種囉!更準確的來說,我們會在最一開始就先把最好吃的那塊蛋糕拿到盤子上、再把第二好吃的那塊蛋糕拿到盤子上……依此類推,也就是說,我們每次拿到盤子上的,都會是貪心地從當前還沒拿進來的蛋糕中拿取其中最好吃的那塊!

這種「每一步都選最好的」做法,我們就稱之為「貪心法」。

貪心法在資訊競賽出現得可多了,但多說無益,就直接讓大家看看貪心法會如何出現在資訊競賽中吧!

例題們


為了讓讀者熟悉一下「跟隨生活直覺的例子」,筆者在這邊準備了幾道題目作為範例。

例題
[Tutorial] 好吃的蛋糕
Source:NCOJ 97

有 $N$ 塊蛋糕,經過你審慎評估後,你了解到對於第 $i$ 塊蛋糕他的好吃度為 $y_i$。但無奈的是由於你食量有限,你只能吃下 $K$ 塊蛋糕。

試問在最佳策略下,若要最大化吃下的蛋糕好吃度總和,這個總和最大可以是多少?

條件限制
  • $1 \leq K \leq N \leq 2\times 10^ 5$
  • $1 \leq y_i \leq 10^ 9$

這正是我們在最一開始用來介紹的基本貪心例題,照著我們提到的邏輯,那就是不斷地把好吃度最大的蛋糕吃掉,直到吃滿 $K$ 塊就行了!

但不對啊?如果執行 $K$ 次的找最大值,不就花了 $O(NK)$ 時間嗎?這樣在 $K$ 很大的時候可是會超時的!

這就是懂設計演算法重要的地方了,我們可以回到我們做法的本質──我們其實是在挑最好吃的那 $K$ 種來吃。

既然如此,這裡就該直接搬出我們的武器,也就是排序!只要做了一次排序後,最大的 $K$ 個數字必然就會被排在後面 $K$ 個位置,所以只要寫個迴圈把他們加總起來就好。這樣我們就能成功在 $O(N\log N)$ 時間內通過這題了!

順帶一提,這題是有辦法達到平均 $O(N)$ 的時間複雜度的……怎麼做呢?也許你會想複習看看基礎演算法 / 標準函式庫 ── <algorithm> 與 <numeric>……

例題
[Tutorial] 趕不完的作業
Source:NCOJ 98

有 $N$ 份作業,你知道每一份作業你都只需要花一小時就能寫完,但可怕的是,第 $i$ 份作業再過 $d_i$ 小時就要截止了!

想要讓所有作業在截止前完成可能還有些困難,因此,你希望能完成越多份作業越好。試問在最佳策略下,你能有幾份作業在截止時間前寫完?

*註:若一份作業一小時後截止,你有辦法馬上開始寫這份作業來趕上死線

條件限制
  • $1 \leq N \leq 2\times 10^ 5$
  • $1 \leq d_i \leq 10^ 9$

這次來了一個全新的問題,我們一樣遵循著直覺──總是做死線最早的那份作業。

相信這也相當的合理,畢竟這些作業花的時間都一樣,那當然就是緊急程度越高的越要先做囉!又如果我們都已經盡全力在趕作業了,卻還是有作業來不及跟上死線,那就沒辦法了嘛。

一樣,拿出我們的排序直接對死線排下去,不過這次當然不是單純的算總合而已,我們還是得好好「模擬」這個寫作業的過程,好好紀錄自己已經花了多少時間,才可以確保下一份死線最早的作業是否已經過期。

例題
誰先晚餐

有 $N$ 位客人來吃飯,其中第一個人的餐點需要煮 $C_i$ 分鐘,且他需要花 $E_i$ 分鐘才能吃完他的餐點。你身為餐廳老闆,你知道你的廚房同一時間只能煮一道料理,但每位客人都可以互相不干涉的享用自己的餐點,而一道料理煮完後便能馬上讓對應的客人開始吃,也就是說,當你在第 $t$ 分鐘開始煮第 $i$ 位客人的餐點,那麼這道餐點會在第 $t+C_i$ 分鐘後煮好,且這位客人一定會在第 $t+C_i+E_i$ 分鐘後享用完畢。

試求在最佳的烹飪順序下,最後一個人享用完他的餐點時間最早可以多早?

條件限制
  • $N\leq 10000$
  • $1\leq C_i, E_i \leq 1000$

題目的參數開始變多了,但不怕,我們一樣試著遵循直覺來找到最佳的策略──那當然是先做需要被吃最久的那道。

不知道大家身邊是否總是有一位朋友的吃飯速度特別慢,每次一群人吃飯就得等他一個,如此想想好像就挺合理的,那當然是先做他的那道菜,才可以讓他有比較充足的時間吃飯囉。

這題的實作也是一樣,先排序之後好好「模擬」做菜的過程就好了,但可別忘了最終的答案是要針對每個人吃完的時間取 $\max$,畢竟就算我們最早就做了吃最慢的人點的菜,他還是有可能吃到大家都吃完了還在吃!

貪心失敗的例子


前面的例題看起來都非常的直覺,不過這樣隨性的講講就真的可以說是正確的嗎?實際在數學上,有很多論述都是需要更加嚴謹的,也有許多數學問題的答案常常違反我們的直覺(可參考三門問題生日悖論)。讓我們來看看以下例題。

例題
物品堆疊

有 $N$ 個物品需要被堆在一個垂直的貨架上,已知第 $i$ 個物品的重量是 $w_i$,需要取用 $f_i$ 次,每次取用一個物品就需要將該物品上方的物品貨架升高才能做取用,因此取用一個物品消耗的能量為其上方的物品重量總和,而取用 $f_i$ 次就要花上 $f_i$ 倍的能量。

請問在最佳的物品擺放順序下,最小的能量消耗總和為何?

條件限制
  • $N\leq 10^5$
  • $1\leq w_i, f_i\leq 1000$

既然重量會被上方的物品影響,那還不簡單?把輕的擺上面就好啦……結果一測範例二由上到下排起來會是

$$
w_i = [3, 4, 5]\\
f_i = [1, 2, 3]
$$

就得到總能量消耗 $27$,居然錯了!

仔細試一下後,就會發現正確的排法應為

$$
w_i = [5, 4, 3]\\
f_i = [3, 2, 1]
$$

才可以真正得到最小的總能力消耗 $19$。

從上面的例子我們可以看到,不是遵循著直覺亂貪心一通就總是會對,就拿前面的「誰先晚餐」當例子,搞不好只是運氣好被我們瞎猜中結論而已!也因此,我們在下一個貪心的章節,會開始教大家如何掌握在一般貪心題目上判斷正確性的一些方法,來讓大家能對自己的「貪心策略」更加的自信。至於上面這題就一起留到下次再講吧!

習題


貪心法看似簡單,但要深入的話是非常有學問的一個領域,我們會在往後的章節慢慢深入,敬請期待。

習題
[Tutorial] 趕不完的作業 2
Source:NCOJ 99

有 $N$ 份作業,你知道每一份作業你都只需要 $L$ 小時就能寫完,但可怕的是,第 $i$ 份作業再過 $d_i$ 小時就要截止了!

想要讓所有作業在截止前完成可能還有些困難,因此,你希望能完成越多份作業越好。試問在最佳策略下,你能有幾份作業在截止時間前寫完?

*註:若一份作業 $L$ 小時後截止,你有辦法馬上開始寫這份作業來趕上死線

條件限制
  • $1 \leq N \leq 2\times 10^ 5$
  • $1 \leq L, d_i \leq 10^ 9$
習題

現有 $n$ 本書需印刷裝訂。每一本書必須先印刷再裝訂。工廠有 $n$ 台裝訂機但只有一台印刷機。印刷機同時只能印一本書,而且必須印完一本書後才能印下一本書。現給定每一本書的印刷時間和裝訂時間,請計算所有書最快要多久才能印刷裝訂完畢。

條件限制
  • $1\leq n \leq 1000$
  • 所有時間都介於 $1$ 到 $1000$ 之間