分发饼干
2024年12月24日大约 7 分钟教学文档贪心算法
题目描述

解题思路
这个问题是一个经典的贪心算法问题。目标是尽可能多地满足孩子的胃口,因此我们需要合理地分配饼干。
求解思路:
贪心策略:
- 为了满足尽可能多的孩子,我们应该优先满足胃口小的孩子,因为他们更容易被满足。
- 对于每一块饼干,我们应该尝试用它来满足当前胃口最小的孩子,这样可以最大化饼干的利用率。
- 因此,我们首先对孩子的胃口值数组
g和饼干尺寸数组s进行升序排序。 - 然后,从胃口最小的孩子开始,尝试用尺寸最小的饼干去满足他。如果这块饼干能满足他,就分配给他,然后继续下一个孩子和下一块饼干。如果不能满足,就尝试下一块更大的饼干。
具体步骤:
- 对
g和s进行升序排序。 - 使用两个指针
i和j,分别指向当前考虑的孩子和当前考虑的饼干。 - 遍历饼干数组
s,对于每一块饼干s[j],尝试满足当前胃口最小的孩子g[i]。 - 如果
s[j] >= g[i],说明这块饼干可以满足这个孩子,计数器count加一,同时i和j都向前移动一位。 - 如果
s[j] < g[i],说明这块饼干太小,无法满足当前孩子,那么这块饼干也无法满足后续胃口更大的孩子,因此只移动j指针,继续寻找更大的饼干。 - 重复此过程,直到遍历完所有饼干或所有孩子。
- 对
Python 代码实现:
def findContentChildren(g, s):
"""
:param g: List[int] - 孩子们的胃口值
:param s: List[int] - 饼干的尺寸
:return: int - 最多能满足的孩子数量
"""
# 1. 排序:对胃口值和饼干尺寸进行升序排序
g.sort()
s.sort()
# 2. 初始化指针和计数器
child_i = 0 # 指向当前考虑的孩子
cookie_j = 0 # 指向当前考虑的饼干
count = 0 # 满足的孩子数量
# 3. 贪心遍历
while child_i < len(g) and cookie_j < len(s):
# 如果当前饼干能满足当前孩子
if s[cookie_j] >= g[child_i]:
# 满足了这个孩子,计数器加一
count += 1
# 移动到下一个孩子
child_i += 1
# 无论是否满足,都移动到下一块饼干
cookie_j += 1
return count
# --- 示例测试 ---
# 示例 1
g1 = [1, 2, 3]
s1 = [1, 1]
print(f"输入: g = {g1}, s = {s1}")
print(f"输出: {findContentChildren(g1, s1)}") # 输出: 1
# 示例 2
g2 = [1, 2]
s2 = [1, 2, 3]
print(f"输入: g = {g2}, s = {s2}")
print(f"输出: {findContentChildren(g2, s2)}") # 输出: 2
代码解释:
g.sort()和s.sort():将孩子的胃口和饼干尺寸从小到大排序,这是贪心策略的基础。child_i和cookie_j是两个指针,用于遍历排序后的数组。while循环确保我们不会超出任何一个数组的边界。if s[cookie_j] >= g[child_i]:是核心判断。如果当前饼干能满足当前孩子,我们就“分配”这块饼干给他(通过增加count和child_i来模拟),然后同时考虑下一个孩子和下一块饼干。- 如果当前饼干不能满足当前孩子,我们不移动
child_i,而是移动cookie_j去寻找更大的饼干。因为当前孩子是胃口最小的未满足者,更小的饼干对他不适用,自然对其他孩子也不适用。 - 最终,
count变量记录的就是能被满足的孩子的最大数量。
注意事项与启发
1. 贪心策略的选择
- 核心启发:这个问题的关键在于如何选择贪心策略。一个直观的想法是“用大饼干满足胃口大的孩子”,但这其实是错误的。正确的贪心策略是“优先满足胃口小的孩子”。因为胃口小的孩子更容易被满足,这能保证我们用最小的资源(饼干)换取最大的效益(满足孩子)。如果用大饼干先去满足胃口大的孩子,可能会浪费资源,导致原本可以满足多个小胃口孩子的饼干被一个大胃口孩子消耗掉。
- 注意:贪心策略的正确性往往需要直觉和经验,有时还需要通过反例来验证其错误性。在本题中,“优先满足胃口大的孩子”就是一个常见的错误思路。
2. 排序的重要性
- 排序是贪心算法的基石:在应用贪心策略之前,对数据进行排序通常是必要的。本题中,对
g(胃口) 和s(饼干尺寸) 进行升序排序,为后续的“优先满足小胃口”策略提供了数据基础。排序后,我们只需要从头到尾遍历一次,就能保证每次操作都是当前最优的局部选择。 - 注意:排序的时间复杂度是 (其中 是孩子数, 是饼干数),这通常是整个算法的主要时间开销。
3. 双指针技巧
- 高效遍历:使用两个指针
child_i和cookie_j同时遍历两个已排序的数组,是一种非常高效的方法。它避免了嵌套循环,将时间复杂度控制在排序的复杂度之下。 - 指针移动逻辑:
- 当
s[cookie_j] >= g[child_i]时,说明当前饼干满足了当前孩子。此时,孩子指针和饼干指针都应向前移动,因为这个孩子已被满足,这块饼干也已被分配。 - 当
s[cookie_j] < g[child_i]时,说明当前饼干无法满足当前孩子。由于数组已排序,这块饼干也无法满足任何后续的孩子(因为他们的胃口更大)。因此,只有饼干指针向前移动,去寻找一块更大的饼干来尝试满足当前孩子。
- 当
- 注意:在编写循环时,必须确保
while循环的条件child_i < len(g) and cookie_j < len(s),以防止数组越界。
4. 边界条件
- 空数组:如果孩子数组
g或饼干数组s为空,函数应直接返回 0。排序和双指针的逻辑自然地处理了这种情况,因为循环条件child_i < len(g) and cookie_j < len(s)在任一数组为空时会立即失效。 - 数量悬殊:当饼干数量远多于孩子数量,或孩子数量远多于饼干数量时,算法依然能正确工作。循环会在较短的数组被遍历完后停止。
5. 代码健壮性
- 原地排序:
g.sort()和s.sort()会修改原始列表。如果题目要求不能修改输入参数,应使用sorted(g)和sorted(s)来创建新的排序列表。 - 数据类型:虽然题目通常假设输入是整数列表,但在实际编程中,应考虑输入数据的有效性(例如,是否包含非数字、负数等),并进行必要的类型检查或异常处理。
6. 启发
- 贪心算法的普遍性:这类“分配”或“调度”问题,常常可以通过贪心策略来解决。核心思想是寻找一种“局部最优”的选择方式,并证明这种选择能导致全局最优。
- 排序 + 双指针:这是一个非常强大的组合,适用于许多需要对两个有序序列进行比较或合并的场景。掌握这种模式有助于解决一系列类似问题,如合并两个有序链表、两数之和的变种等。
- 从反例中学习:当设计贪心策略时,尝试用反例来验证其正确性是一个好习惯。例如,思考“如果我这样做会怎么样?”并尝试构造一个反例来推翻它,这有助于加深对问题本质的理解。
