目录

题目描述

135. 分发糖果

题意分析

一排孩子站成一列,每个孩子有一个评分 ratings[i]。要给每人发糖,问在满足规则的前提下,最少一共要发多少颗。

规则只有两条,但第二条藏着这道题的全部难度。第一条是每个孩子至少分到一颗糖,这给了所有位置一个下界 1。第二条是「相邻的两个孩子中,评分更高的那个必须拿到更多糖果」——注意它说的是「相邻两个」,而不是「比左边的多」。这意味着对位置 i 来说,它同时被左右两个邻居约束:如果 ratings[i] > ratings[i - 1],那么 candies[i] > candies[i - 1];如果 ratings[i] > ratings[i + 1],那么 candies[i] > candies[i + 1]这是一个双向约束,而不是单向的递推关系,读题时如果把它当成「从左往右越来越高就越给越多」,后面必然出错。

反过来说,评分相等时规则不生效。ratings[i] == ratings[i - 1] 时两人可以拿一样多,甚至可以一个多一个少,规则对相等的情况完全没有要求。这一点是很多贪心题里最省糖的地方,也是最容易被误加约束的地方。

边界方面:数组长度至少为 1,单个孩子直接返回 1;评分可以完全相同,此时答案就是 n;评分可以单调递增或单调递减,这两种极端形态会让某一趟扫描把整条链都串起来,是验证代码的好用例。题目求的是「最少」,所以每个位置都应该压到它的约束允许的最小值,多给一颗就不是最优解。

解法:左右两次扫描取最大约束

核心思路

问题关键:每个孩子同时受左右邻居约束。只从左向右扫描能保证“评分高于左邻居时糖更多”,却无法保证下降段中的右侧约束;反向扫描同理,所以一趟贪心信息不足。

为什么选两次扫描:先把每人初始化为 1。第一趟从左到右处理所有上升关系;第二趟从右到左处理所有下降关系。第二趟使用 max(当前值, 右邻居+1),在补足右约束时保留第一趟已经建立的左约束。

状态与不变量:第一趟结束后,candies[i] 是仅考虑左邻居时的最小合法值。第二趟扫描到 i 时,candies[i+1] 已满足两侧约束;更新后的 candies[i] 同时不小于左右两个方向要求的下界。

正确性:设只考虑左、右邻居时的最小值分别为 L[i]R[i],任何合法方案都必须满足 candy[i] ≥ L[i]candy[i] ≥ R[i],所以至少为 max(L[i],R[i])。两趟扫描恰好得到这个最大值,它同时满足两侧约束,因此逐位置都不能再减少,总和就是全局最小。

解题步骤

  • 创建 candies 并全部填 1,落实每个孩子的最低糖果数。
  • 从左到右:若当前评分更高,令当前糖果为左邻居加 1。
  • 从右到左:若当前评分更高,令当前糖果为“原值”和“右邻居加 1”的较大者。
  • 累加 candies 得到最少总数;评分相等时两趟都不更新。
  • 口述样例[1,0,2] 初始化为 [1,1,1],左扫得到 [1,1,2],右扫补成 [2,1,2],答案为 5。[1,2,2] 得到 [1,2,1],说明相等评分不要求糖果递增。

代码实现

class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] candies = new int[n];
        Arrays.fill(candies, 1);

        for (int i = 1; i < n; i++) {
            if (ratings[i] > ratings[i - 1]) {
                candies[i] = candies[i - 1] + 1;
            }
        }

        for (int i = n - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                // 同时满足左侧扫描和右侧扫描给出的最小约束。
                candies[i] = Math.max(candies[i], candies[i + 1] + 1);
            }
        }

        int ans = 0;
        for (int candy : candies) {
            ans += candy;
        }
        return ans;
    }
}
func candy(ratings []int) int {
    n := len(ratings)
    candies := make([]int, n)
    for i := 0; i < n; i++ {
        candies[i] = 1
    }

    for i := 1; i < n; i++ {
        if ratings[i] > ratings[i-1] {
            candies[i] = candies[i-1] + 1
        }
    }
    for i := n - 2; i >= 0; i-- {
        if ratings[i] > ratings[i+1] {
            needed := candies[i+1] + 1
            if candies[i] < needed {
                candies[i] = needed
            }
        }
    }

    ans := 0
    for _, candy := range candies {
        ans += candy
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(n)$,两次扫描和一次求和都为线性时间。
  • 空间复杂度:$O(n)$,使用一个与评分数组等长的糖果数组。

关键点总结

  • 双向局部约束可以拆成两次相反方向的单向传播。
  • 第二趟必须取最大值,不能覆盖第一趟形成的左侧约束。
  • 只有评分严格更高才增加糖果;相等评分没有大小要求。
  • 面试追问 $O(1)$ 空间时,可按连续上升、下降段计数并用等差数列求和;主解法优先保证清晰和稳定。

易错点总结

  • 只做左扫描[1,0,2] 会得到 [1,1,2],位置 0 未满足右侧约束。
  • 右扫描直接覆盖[1,2,3,1] 会把峰值 3 降成 2,破坏已满足的左侧约束。
  • 右扫描方向写反:长下降段依赖右邻居的最终值,必须从右向左传播。
  • 数组未初始化为 1:全相等输入会错误返回 0,而每个孩子至少需要一颗。
  • 比较使用 >=[1,1,1] 会被多发糖果;评分相等时规则不生效。

相似题目

题目 难度 考察点
238. 除了自身以外数组的乘积 中等 同样是左右两趟,但合并方式是相乘而非取 max
42. 接雨水 困难 双向扫描求左右最大值,合并时取的是较小者
55. 跳跃游戏 中等 单向贪心即可,维护能到达的最远边界
45. 跳跃游戏 II 中等 贪心分层,按当前层的右边界切分步数
763. 划分字母区间 中等 先预处理末次位置,再一趟扫描确定切分点
406. 根据身高重建队列 中等 贪心的关键在排序顺序,插入时才满足计数约束