LeetCode 135 · 分发糖果:两次扫描,先左后右取最大

这道题乍一看需要同时考虑左右两边的约束,很容易陷入"我先确定左边再确定右边,但确定右边又破坏了左边"的循环。但其实只要拆成两遍扫描------一遍只看左边,一遍只看右边,最后取最大值------问题就迎刃而解了。这个"左右两遍扫描"的套路在 LeetCode 238(除自身以外数组的乘积)里也用过。


题目长什么样

n 个孩子站成一排,给你一个数组 ratings 表示每个孩子的评分。分发糖果需要满足:

  1. 每个孩子至少 1 颗糖果
  2. 相邻两个孩子中,评分更高 的那个获得更多糖果

返回需要准备的最少糖果数目

输入ratings = [1,0,2]

输出5(分发 [2,1,2]

说人话就是:找一组糖果数,让评分高的相邻孩子拿得多,同时总数最少。


关键观察:相邻关系有两个方向

规则说"相邻两个孩子中评分更高的获得更多"。这句话拆开来看:

text 复制代码
对于位置 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],就让 candies[i] = candies[i-1] + 1

python 复制代码
candies = [1] * n
for i in range(1, n):
    if ratings[i] > ratings[i - 1]:
        candies[i] = candies[i - 1] + 1

第二遍:从右到左

只考虑"左边比右边大"的情况。如果 ratings[i] > ratings[i+1],就让 candies[i] = max(candies[i], candies[i+1] + 1)

为什么要取 max? 因为第一遍已经定了一个值,第二遍只能在原有基础上增大,不能减小------否则就破坏了第一遍已经满足的"左到右"约束。

python 复制代码
for i in range(n - 2, -1, -1):
    if ratings[i] > ratings[i + 1]:
        candies[i] = max(candies[i], candies[i + 1] + 1)

完整代码

python 复制代码
class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candies = [1] * n

        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candies[i] = candies[i - 1] + 1

        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                candies[i] = max(candies[i], candies[i + 1] + 1)

        return sum(candies)

跑一遍示例 1

text 复制代码
ratings = [1, 0, 2]

初始化: candies = [1, 1, 1]

第一遍(从左到右):
  i=1: ratings[1]=0 < ratings[0]=1 → 不动
  i=2: ratings[2]=2 > ratings[1]=0 → candies[2] = 1+1 = 2
  candies = [1, 1, 2]

第二遍(从右到左):
  i=1: ratings[1]=0 < ratings[2]=2 → 不动
  i=0: ratings[0]=1 > ratings[1]=0 → candies[0] = max(1, 1+1) = 2
  candies = [2, 1, 2]

最少糖果数 = 2 + 1 + 2 = 5 ✓

跑一遍示例 2

text 复制代码
ratings = [1, 2, 2]

初始化: candies = [1, 1, 1]

第一遍(从左到右):
  i=1: ratings[1]=2 > ratings[0]=1 → candies[1] = 1+1 = 2
  i=2: ratings[2]=2 = ratings[1]=2 → 不动("严格大于"才需要更多)
  candies = [1, 2, 1]

第二遍(从右到左):
  i=1: ratings[1]=2 > ratings[2]=2? → 不动
  i=0: ratings[0]=1 < ratings[1]=2 → 不动
  candies = [1, 2, 1]

最少糖果数 = 1 + 2 + 1 = 4 ✓

注意示例 2 的关键点:评分相等不需要更多糖果 。所以 ratings[1]=2ratings[2]=2 之间没有约束,各拿各的。

维度 说明
时间 O(n) 两遍扫描
空间 O(n) candies 数组

优化一:把 sum 合并进第二遍扫描

基础版最后用 sum(candies) 单独算总数,相当于多了一次 O(n) 遍历。其实在第二遍扫描时顺手累加就行------少一次遍历,常数项更优。

python 复制代码
class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candies = [1] * n

        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candies[i] = candies[i - 1] + 1

        total = candies[-1]
        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                candies[i] = max(candies[i], candies[i + 1] + 1)
            total += candies[i]
        return total

关键改动totalcandies[-1] 开始累加(最后一项一定不会被第二遍改动),然后在循环里 total += candies[i]

维度 说明
时间 O(n) 真正的两遍扫描,不是三遍
空间 O(n) candies 数组

实测:LeetCode 上比基础版快约 30%,从 20ms 提到 ~14ms。


优化二:一次遍历,O(1) 空间

通过观察上升和下降序列的长度,可以在 O(1) 空间内完成。代码复杂度上去了,但内存占用从 21MB 降到 17MB 左右。

python 复制代码
class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        if n <= 1:
            return n
        total = 0
        up = down = peak = 0
        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                up += 1
                peak = up
                down = 0
                total += up + 1
            elif ratings[i] < ratings[i - 1]:
                down += 1
                up = 0
                total += down + (1 if down > peak else 0)
            else:
                up = down = peak = 0
                total += 1
        return total + 1

思路

维护三个变量:

变量 含义
up 当前上升序列的长度(不含起点)
down 当前下降序列的长度(不含起点)
peak 上一次上升序列的最大长度(用于处理峰顶)

每一步根据斜率(上升/下降/平)累加糖果数:

  • 上升 :当前孩子的糖果数 = 上升序号,直接累加 up + 1(加 1 是起点)
  • 下降 :当前孩子的糖果数 = 下降序号。如果下降段超过了上升段的峰值 peak,峰顶要额外 +1
  • :相邻评分相等,互不影响,当前孩子拿 1 颗

关键点:down > peak 时 +1 。因为下降段比上升段长时,峰顶的糖果数要从 peak + 1 提到 down + 1(被下降段反向"拉高")。

维度 说明
时间 O(n) 一次遍历
空间 O(1) 只用了三个变量

三种解法放在一起看

解法 时间 空间 用时(参考) 内存(参考) 适用场景
基础两次扫描 O(n) O(n) ~20ms ~21MB 易读,基准
合并 sum O(n) O(n) ~14ms ~21MB 省一次遍历,提速
一次遍历 O(n) O(1) ~16ms ~17MB 省内存

面试时推荐"合并 sum"版------思路和基础版一样清晰,但常数更优。一次遍历版适合在面试官追问空间优化时拿出来。


这道题教会我什么

"左右两遍扫描"是一个万能套路

这道题和 LeetCode 238(除自身以外数组的乘积)、LeetCode 42(接雨水)是同一个套路:

text 复制代码
第一遍从左到右:累积左边的信息
第二遍从右到左:累积右边的信息
合并:取最大值或乘积

只要问题可以拆成"左半边 + 右半边"的形式,这个套路就能用。

"严格大于"还是"大于等于"要分清

这道题的规则是"评分更高 → 糖果更多",对应严格大于>)。所以 ratings=[1,2,2] 中两个 2 之间没有约束。如果误用 >=,就会强制让后面的孩子拿更多糖果,导致总数偏大。读题时一定要看清"严格"二字。

第二遍用 max 而不是赋值

第二遍从右到左扫描时,必须用 max(candies[i], candies[i+1]+1),而不是直接 candies[i] = candies[i+1] + 1。因为第一遍已经满足了"左到右"的约束,第二遍只能在原有基础上增大,不能减小------否则就破坏了已经满足的条件。这种"取最大值合并约束"的思想在很多 DP 和贪心题里都有。


完整测试代码

python 复制代码
from typing import List


class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candies = [1] * n

        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candies[i] = candies[i - 1] + 1

        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                candies[i] = max(candies[i], candies[i + 1] + 1)

        return sum(candies)


if __name__ == "__main__":
    s = Solution()

    ratings = [1, 0, 2]
    print(f"ratings={ratings} → {s.candy(ratings)}")

    ratings = [1, 2, 2]
    print(f"ratings={ratings} → {s.candy(ratings)}")

    ratings = [1, 2, 3, 4, 5]
    print(f"ratings={ratings} → {s.candy(ratings)}")

    ratings = [5, 4, 3, 2, 1]
    print(f"ratings={ratings} → {s.candy(ratings)}")

    ratings = [1, 3, 2, 2, 1]
    print(f"ratings={ratings} → {s.candy(ratings)}")

    ratings = [1]
    print(f"ratings={ratings} → {s.candy(ratings)}")

相关题目推荐

相关推荐
西安邮电大学1 小时前
贪心算法详细讲解
java·后端·其他·算法·面试
退休倒计时2 小时前
【每日一题】LeetCode 19. 删除链表的倒数第 N 个结点 TypeScript
leetcode·链表·typescript
装不满的克莱因瓶2 小时前
掌握生成对抗网络(GAN)的优化目标与评估指标——从博弈函数到生成质量衡量体系
人工智能·python·深度学习·算法·机器学习
技术小黑2 小时前
CNN算法实战系列06 | InceptionV1实现猴痘病识别
深度学习·算法·cnn·inceptionv1
云淡风轻~窗明几净2 小时前
角谷猜想的任意算法测试
数据结构·人工智能·算法
happygrilclh3 小时前
赚外快了:等离子表面处理机电源算法需求说明
算法
ji198594433 小时前
MATLAB 求散点曲线斜率
开发语言·算法·matlab
kaikaile19953 小时前
MATLAB 实现:Koch & Zhao 图像水印算法(DCT域)
开发语言·算法·matlab
QiLinkOS3 小时前
QiLink开源生态的三维重构:基于时间、空间与社会价值的底层规则创新白皮书
大数据·c++·人工智能·科技·算法·gitee·开源