好的,孩子,我们来玩一个“喂饼干”的游戏。
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 个孩子。
这个策略之所以有效,是因为它总是用“勉强够用”的最小饼干去满足胃口最小的孩子,从而把更有潜力的饼干留给后续的孩子。