LeetCode 135.分发糖果:双向遍历下的贪心策略应用
一、问题定义与核心约束
1.1 问题描述
有 n
个孩子站成一排,每个孩子都有一个评分 ratings
。需要按照以下规则给孩子们分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻两个孩子中,评分高的孩子必须获得更多的糖果。
要求返回最少需要的糖果总数。
1.2 核心挑战
- 单向约束可能导致冲突:例如,当一个孩子同时比左边和右边的孩子评分高时,需同时满足两边的糖果数量要求
- 需找到满足所有约束的最小糖果分配方案,避免过度分配
示例:
- 输入:
ratings = [1,0,2]
- 输出:
5
(分配方案:[2,1,2]
)
二、解题思路:双向贪心的分步满足策略
2.1 核心思想
通过两次遍历分别满足左右方向的约束,最终取两次约束的最大值作为每个孩子的糖果数:
- 左→右遍历:确保每个孩子比左边评分高的孩子获得更多糖果
- 右→左遍历:确保每个孩子比右边评分高的孩子获得更多糖果
- 每个孩子的最终糖果数为两次遍历结果的最大值,同时满足两个方向的约束
2.2 为什么需要双向遍历?
- 单向遍历无法处理"同时比左右邻居评分高"的情况(如
[1,3,2]
) - 分两次遍历可分别固定一个方向的参考基准,确保被比较方的糖果数量已确定
三、代码逐行解析
3.1 初始化糖果数组
int[] candies = new int[ratings.length];
Arrays.fill(candies, 1); // 每个孩子至少1个糖果
- 初始值设为1,满足"每个孩子至少1个糖果"的基本约束
3.2 左→右遍历:满足右侧比左侧评分高的约束
for (int i = 1; i < candies.length; i++) {// 若当前孩子评分高于左边,糖果数比左边多1if (ratings[i] > ratings[i - 1]) {candies[i] = candies[i - 1] + 1;}
}
- 关键逻辑:此时左边孩子的糖果数已确定(因从左向右遍历),可直接作为参考基准
- 示例:
ratings = [1,0,2]
经左→右遍历后,candies = [1,1,2]
3.3 右→左遍历:满足左侧比右侧评分高的约束
for (int i = candies.length - 2; i >= 0; i--) {// 若当前孩子评分高于右边,取"当前值"与"右边+1"的最大值if (ratings[i] > ratings[i + 1]) {candies[i] = Math.max(candies[i], candies[i + 1] + 1);}
}
- 关键逻辑:此时右边孩子的糖果数已确定(因从右向左遍历),通过
Math.max
确保同时满足左→右遍历的结果 - 示例:
ratings = [1,0,2]
经右→左遍历后,candies = [2,1,2]
(处理了ratings[0] > ratings[1]
的约束)
3.4 计算总糖果数
return Arrays.stream(candies).sum(); // 返回所有糖果的总和
四、算法执行过程演示
以 ratings = [1,3,2,1]
为例:
步骤1:初始化
candies = [1,1,1,1]
步骤2:左→右遍历
i=1
:ratings[1]=3 > ratings[0]=1
→candies[1] = 1+1=2
→[1,2,1,1]
i=2
:ratings[2]=2 < ratings[1]=3
→ 不变 →[1,2,1,1]
i=3
:ratings[3]=1 < ratings[2]=2
→ 不变 →[1,2,1,1]
步骤3:右→左遍历
i=2
:ratings[2]=2 > ratings[3]=1
→max(1, 1+1)=2
→[1,2,2,1]
i=1
:ratings[1]=3 > ratings[2]=2
→max(2, 2+1)=3
→[1,3,2,1]
i=0
:ratings[0]=1 < ratings[1]=3
→ 不变 →[1,3,2,1]
步骤4:总和计算
1+3+2+1=7
→ 最终结果为 7
五、核心逻辑深度解析
5.1 为什么被比较方的糖果数必须已确定?
- 左→右遍历时:比较对象是左边的孩子(
i-1
),由于遍历顺序是从左到右,i-1
的糖果数在之前的步骤中已确定,可直接作为参考 - 右→左遍历时:比较对象是右边的孩子(
i+1
),由于遍历顺序是从右到左,i+1
的糖果数在之前的步骤中已确定,可直接作为参考 - 这种"已确定状态"保证了每次赋值都是基于可靠的基准,避免冲突
5.2 为什么要用 Math.max
处理右→左遍历?
- 左→右遍历可能已为当前位置赋予了一个满足左侧约束的值(如
candies[i]
可能已经大于candies[i+1]+1
) - 例如:
ratings = [5,4,3,6]
左→右遍历后candies = [1,1,1,2]
,右→左遍历到i=2
时,ratings[2]=3 < ratings[3]=6
无需处理;遍历到i=0
时,ratings[0]=5 > ratings[1]=4
,但candies[0]=1
需更新为max(1, 1+1)=2
5.3 贪心策略的体现
- 每次遍历只关注一个方向的约束,确保局部最优(满足当前方向的最小糖果数)
- 最终通过取最大值合并两个方向的约束,实现全局最优(总糖果数最少)
六、算法复杂度分析
- 时间复杂度:
O(n)
,其中n
是孩子数量。两次遍历数组各需O(n)
时间,求和操作也为O(n)
- 空间复杂度:
O(n)
,需要一个长度为n
的数组存储每个孩子的糖果数
七、常见误区与优化说明
7.1 误区1:试图通过一次遍历解决
单向遍历无法处理"同时需要比左右邻居多"的情况,例如 ratings = [2,1,3]
:
- 左→右遍历会得到
[1,1,2]
,但ratings[0] > ratings[1]
未满足 - 必须通过右→左遍历补充处理
7.2 误区2:忽略 Math.max
的作用
右→左遍历时直接赋值 candies[i] = candies[i+1] + 1
会覆盖左→右遍历的结果,可能破坏已满足的左侧约束。
7.3 优化点:空间复杂度优化
可通过两个变量替代数组(分别记录当前和前一个位置的糖果数),将空间复杂度降至 O(1)
,但会增加逻辑复杂度。
八、总结:双向约束问题的解题范式
本题通过分阶段处理双向约束的思路,为类似问题提供了通用解法:
- 识别问题中的双向约束(如本题中"左高右低"和"右高左低"两种情况)
- 分两次遍历,每次处理一个方向的约束,确保被比较方的状态已确定
- 合并两次遍历的结果,取满足所有约束的最小值
这种思路的核心是将复杂的全局约束分解为可分步处理的局部约束,通过贪心策略实现高效求解。