这道题乍一看需要同时考虑左右两边的约束,很容易陷入"我先确定左边再确定右边,但确定右边又破坏了左边"的循环。但其实只要拆成两遍扫描------一遍只看左边,一遍只看右边,最后取最大值------问题就迎刃而解了。这个"左右两遍扫描"的套路在 LeetCode 238(除自身以外数组的乘积)里也用过。
题目长什么样
n 个孩子站成一排,给你一个数组 ratings 表示每个孩子的评分。分发糖果需要满足:
- 每个孩子至少 1 颗糖果
- 相邻两个孩子中,评分更高 的那个获得更多糖果
返回需要准备的最少糖果数目。
输入 :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]=2 和 ratings[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
关键改动 :total 从 candies[-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)}")
相关题目推荐:
- LeetCode 238 · 除自身以外数组的乘积(同样的左右两遍扫描)
- LeetCode 42 · 接雨水(左右两遍扫描的经典应用)
- LeetCode 134 · 加油站(贪心思路的相似题目)