好的,孩子,我們來玩一個“喂餅干”的游戲。
0. 問題的本質是什么?
想象一下,你就是個超棒的家長,手里有幾塊大小不一的餅干,而面前有幾個餓著肚子的小朋友。每個小朋友都有一個最小的“胃口”值,比如有的孩子胃口小,只要一塊尺寸為 1 的餅干就滿足了;有的孩子胃口大,得要一塊尺寸至少為 3 的餅干才行。
你的任務很簡單:用你手里的餅干,盡可能地讓更多的孩子吃飽。但是有個規矩:每個孩子只能分到一塊餅干。
- 孩子們:他們的“胃口”是一個數字列表
g
。 - 餅干們:它們的“尺寸”也是一個數字列表
s
。 - 滿足條件:一塊餅干的尺寸
s[j]
,必須大于或等于一個孩子的胃口g[i]
,這個孩子才會被滿足。
你的最終目標是找出你最多能滿足多少個孩子。
1. 為什么“貪心”在這里能成功?
這次,我們不用像之前“背包問題”那么復雜的動態規劃了。因為這個“喂餅干”的問題有一個很好的性質,我們可以用一個更簡單的策略來解決,這個策略叫做貪心算法。
貪心算法的思路是:
每一步都做出當前看來最好的選擇,希望這些局部的最佳選擇能夠導致最終的全局最佳結果。
那么,這里的“最佳選擇”是什么呢?
有兩種直覺:
- 從大到小喂:用最大的餅干去喂胃口最大的孩子。
- 從小到大喂:用最小的餅干去喂胃口最小的孩子。
我們來分析一下,為什么“從小到大喂”是一個更好的策略。
假設你有一塊尺寸為 5 的大餅干和一塊尺寸為 1 的小餅干。你面前有兩個孩子,一個胃口為 4,一個胃口為 1。
-
如果你用大餅干(5)去喂大胃口的孩子(4):
- 這個孩子滿足了,你還剩下小餅干(1)和另一個小胃口的孩子(1)。
- 你用剩下的餅干去喂剩下的孩子,這個孩子也滿足了。
- 結果:兩個孩子都滿足了。
-
如果你用小餅干(1)去喂小胃口的孩子(1):
- 這個孩子滿足了,你還剩下大餅干(5)和另一個大胃口的孩子(4)。
- 你用剩下的餅干去喂剩下的孩子,這個孩子也滿足了。
- 結果:兩個孩子都滿足了。
從這個例子看,兩種方法都行。但是,再看一個情況:
假設你有一塊尺寸為 5 的大餅干和一塊尺寸為 2 的小餅干。你面前有兩個孩子,一個胃口為 4,一個胃口為 2。
-
如果你用大餅干(5)去喂大胃口的孩子(4):
- 這個孩子滿足了,你剩下小餅干(2)和另一個小胃口的孩子(2)。
- 你用剩下的餅干去喂剩下的孩子,這個孩子也滿足了。
- 結果:兩個孩子都滿足了。
-
如果你用小餅干(2)去喂小胃口的孩子(2):
- 這個孩子滿足了,你剩下大餅干(5)和另一個大胃口的孩子(4)。
- 你用剩下的餅干去喂剩下的孩子,這個孩子也滿足了。
- 結果:兩個孩子都滿足了。
看起來好像都一樣。那我們換個角度想:
- 如果我有一塊尺寸為 2 的小餅干,它能喂飽胃口為 2 的孩子,但喂不飽胃口為 4 的孩子。
- 如果我有一塊尺寸為 5 的大餅干,它既能喂飽胃口為 2 的孩子,也能喂飽胃口為 4 的孩子。
你覺得,那塊尺寸為 5 的大餅干是不是很寶貴?它能喂飽那些挑剔的大胃口孩子,而小餅干不行。如果我一開始就把大餅干浪費在小胃口孩子身上,那么后面大胃口的孩子就可能餓著了。
所以,最好的策略是:
用最小的餅干,去滿足胃口最小的孩子。
這樣,我們就能把大餅干省下來,留給那些胃口更大的孩子。這個策略就是“貪心”的精髓。
2. 算法步驟和推演過程
根據上面的“貪心”策略,我們來一步步解決這個問題。
**第一步:**把孩子們按胃口從小到大排隊,把餅干們按尺寸從小到大排隊。
g = [1, 2, 3]
-> 排序后g = [1, 2, 3]
s = [1, 1]
-> 排序后s = [1, 1]
**第二步:**從小到大,一個一個地嘗試用餅干去滿足孩子。
我們用兩個指針,一個指向最小胃口的孩子(child_index
),一個指向最小尺寸的餅干(cookie_index
)。
child_index
= 0 (指向胃口為 1 的孩子)cookie_index
= 0 (指向尺寸為 1 的餅干)- 滿足的孩子數量
satisfied_count
= 0
開始匹配:
-
孩子
g[0]
的胃口是 1,餅干s[0]
的尺寸是 1。s[0]
(1) >=g[0]
(1)?是的,滿足!- 我們將這塊餅干給這個孩子。
satisfied_count
增加到 1。- 孩子和餅干的指針都往前走一步:
child_index
= 1 (指向胃口為 2 的孩子)cookie_index
= 1 (指向尺寸為 1 的餅干)
-
孩子
g[1]
的胃口是 2,餅干s[1]
的尺寸是 1。s[1]
(1) >=g[1]
(2)?不是,不滿足。- 這塊餅干太小了,喂不飽這個孩子。
- 怎么辦?我們不能把這塊小餅干留給這個孩子,因為他吃不飽。但是這塊餅干也喂不飽后面的任何一個孩子(因為后面的孩子胃口只會更大)。所以,這塊餅干只能扔掉。
- 我們只讓餅干的指針往前走一步:
child_index
保持不變 (指向胃口為 2 的孩子)cookie_index
= 2 (沒有更多餅干了)
-
餅干用完了!
cookie_index
已經超出餅干列表的范圍了。游戲結束。
最終結果:
我們成功滿足了 1 個孩子。
再來一個例子:
g = [1, 2]
,s = [1, 2, 3]
- 排序后:
g = [1, 2]
,s = [1, 2, 3]
child_index
= 0,cookie_index
= 0,satisfied_count
= 0
開始匹配:
-
孩子
g[0]
(1),餅干s[0]
(1)。s[0]
(1) >=g[0]
(1)?是的,滿足!satisfied_count
= 1。child_index
= 1,cookie_index
= 1。
-
孩子
g[1]
(2),餅干s[1]
(2)。s[1]
(2) >=g[1]
(2)?是的,滿足!satisfied_count
= 2。child_index
= 2,cookie_index
= 2。
-
孩子用完了!
child_index
已經超出孩子列表的范圍了。游戲結束。
最終結果:
我們成功滿足了 2 個孩子。
這個策略之所以有效,是因為它總是用“勉強夠用”的最小餅干去滿足胃口最小的孩子,從而把更有潛力的餅干留給后續的孩子。