跳至主要內容

分发饼干

周子力2024年12月24日大约 7 分钟教学文档贪心算法

题目描述

picture 0
picture 0

解题思路

这个问题是一个经典的贪心算法问题。目标是尽可能多地满足孩子的胃口,因此我们需要合理地分配饼干。

求解思路:

  1. 贪心策略

    • 为了满足尽可能多的孩子,我们应该优先满足胃口小的孩子,因为他们更容易被满足。
    • 对于每一块饼干,我们应该尝试用它来满足当前胃口最小的孩子,这样可以最大化饼干的利用率。
    • 因此,我们首先对孩子的胃口值数组 g 和饼干尺寸数组 s 进行升序排序。
    • 然后,从胃口最小的孩子开始,尝试用尺寸最小的饼干去满足他。如果这块饼干能满足他,就分配给他,然后继续下一个孩子和下一块饼干。如果不能满足,就尝试下一块更大的饼干。
  2. 具体步骤

    • gs 进行升序排序。
    • 使用两个指针 ij,分别指向当前考虑的孩子和当前考虑的饼干。
    • 遍历饼干数组 s,对于每一块饼干 s[j],尝试满足当前胃口最小的孩子 g[i]
    • 如果 s[j] >= g[i],说明这块饼干可以满足这个孩子,计数器 count 加一,同时 ij 都向前移动一位。
    • 如果 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

代码解释:

  1. g.sort()s.sort():将孩子的胃口和饼干尺寸从小到大排序,这是贪心策略的基础。
  2. child_icookie_j 是两个指针,用于遍历排序后的数组。
  3. while 循环确保我们不会超出任何一个数组的边界。
  4. if s[cookie_j] >= g[child_i]: 是核心判断。如果当前饼干能满足当前孩子,我们就“分配”这块饼干给他(通过增加 countchild_i 来模拟),然后同时考虑下一个孩子和下一块饼干。
  5. 如果当前饼干不能满足当前孩子,我们不移动 child_i,而是移动 cookie_j 去寻找更大的饼干。因为当前孩子是胃口最小的未满足者,更小的饼干对他不适用,自然对其他孩子也不适用。
  6. 最终,count 变量记录的就是能被满足的孩子的最大数量。

注意事项与启发

1. 贪心策略的选择

  • 核心启发:这个问题的关键在于如何选择贪心策略。一个直观的想法是“用大饼干满足胃口大的孩子”,但这其实是错误的。正确的贪心策略是“优先满足胃口小的孩子”。因为胃口小的孩子更容易被满足,这能保证我们用最小的资源(饼干)换取最大的效益(满足孩子)。如果用大饼干先去满足胃口大的孩子,可能会浪费资源,导致原本可以满足多个小胃口孩子的饼干被一个大胃口孩子消耗掉。
  • 注意:贪心策略的正确性往往需要直觉和经验,有时还需要通过反例来验证其错误性。在本题中,“优先满足胃口大的孩子”就是一个常见的错误思路。

2. 排序的重要性

  • 排序是贪心算法的基石:在应用贪心策略之前,对数据进行排序通常是必要的。本题中,对 g (胃口) 和 s (饼干尺寸) 进行升序排序,为后续的“优先满足小胃口”策略提供了数据基础。排序后,我们只需要从头到尾遍历一次,就能保证每次操作都是当前最优的局部选择。
  • 注意:排序的时间复杂度是 O(NlogN+MlogM)O(N \log N + M \log M)(其中 NN 是孩子数,MM 是饼干数),这通常是整个算法的主要时间开销。

3. 双指针技巧

  • 高效遍历:使用两个指针 child_icookie_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. 启发

  • 贪心算法的普遍性:这类“分配”或“调度”问题,常常可以通过贪心策略来解决。核心思想是寻找一种“局部最优”的选择方式,并证明这种选择能导致全局最优。
  • 排序 + 双指针:这是一个非常强大的组合,适用于许多需要对两个有序序列进行比较或合并的场景。掌握这种模式有助于解决一系列类似问题,如合并两个有序链表、两数之和的变种等。
  • 从反例中学习:当设计贪心策略时,尝试用反例来验证其正确性是一个好习惯。例如,思考“如果我这样做会怎么样?”并尝试构造一个反例来推翻它,这有助于加深对问题本质的理解。
上次编辑于: 2025/11/12 08:34:11
贡献者: zilizhou,keep