Article / 文章
LeetCode 第135题:分发糖果
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果: - 每个孩子至少分配到 1 个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
题目描述
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
难度
困难
题目链接
示例
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题目要求。
提示
n == ratings.length1 <= n <= 2 * 10^40 <= ratings[i] <= 2 * 10^4
解题思路
方法一:两次遍历
这道题要求我们给每个孩子分发糖果,满足两个条件:每个孩子至少有1个糖果,评分高的孩子比相邻的评分低的孩子有更多的糖果。
关键点:
- 从左到右遍历,确保右边评分更高的孩子比左边的孩子糖果数更多
- 从右到左遍历,确保左边评分更高的孩子比右边的孩子糖果数更多
- 取两次遍历结果的最大值,确保同时满足两个方向的要求
具体步骤:
- 初始化一个长度为n的数组candies,所有元素初始化为1(每个孩子至少有1个糖果)
- 从左到右遍历ratings数组:
- 如果ratings[i] > ratings[i-1],则candies[i] = candies[i-1] + 1
- 从右到左遍历ratings数组:
- 如果ratings[i] > ratings[i+1],则candies[i] = max(candies[i], candies[i+1] + 1)
- 计算candies数组的总和,即为最少需要的糖果数
时间复杂度:O(n),其中n是孩子的数量。需要遍历两次ratings数组。 空间复杂度:O(n),需要一个长度为n的数组存储每个孩子分得的糖果数。
方法二:常数空间的一次遍历
我们可以通过观察发现,糖果分配的结果实际上是由一个个“上升”和“下降”的序列组成的。
关键点:
- 上升序列:从左到右评分递增,糖果数也递增
- 下降序列:从左到右评分递减,糖果数也递减
- 平坦序列:评分相等,糖果数重置为1
具体步骤:
- 初始化总糖果数为1(第一个孩子至少有1个糖果)
- 初始化当前糖果数为1,上升序列长度为0,下降序列长度为0
- 遍历ratings数组(从第二个孩子开始):
- 如果ratings[i] > ratings[i-1](上升),当前糖果数加1,更新总糖果数
- 如果ratings[i] < ratings[i-1](下降),下降序列长度加1,更新总糖果数
- 如果ratings[i] == ratings[i-1](平坦),当前糖果数重置为1,更新总糖果数
- 返回总糖果数
时间复杂度:O(n),其中n是孩子的数量。只需要遍历一次ratings数组。 空间复杂度:O(1),只需要常数级别的额外空间。
图解思路
两次遍历分析表
以示例1为例:ratings = [1,0,2]
第一次遍历(从左到右)
| 索引 | ratings[i] | 比较 | candies[i] | 说明 |
|---|---|---|---|---|
| 0 | 1 | - | 1 | 第一个孩子,至少有1个糖果 |
| 1 | 0 | 0 < 1 | 1 | 评分比左边低,保持1个糖果 |
| 2 | 2 | 2 > 0 | 2 | 评分比左边高,糖果数为左边加1 |
第二次遍历(从右到左)
| 索引 | ratings[i] | 比较 | candies[i] | 说明 |
|---|---|---|---|---|
| 2 | 2 | - | 2 | 最右边的孩子,保持2个糖果 |
| 1 | 0 | 0 < 2 | 1 | 评分比右边低,保持1个糖果 |
| 0 | 1 | 1 > 0 | max(1, 1+1) = 2 | 评分比右边高,糖果数为右边加1和当前值的最大值 |
最终结果:[2,1,2],总糖果数:2 + 1 + 2 = 5
上升下降序列分析表
以示例1为例:ratings = [1,0,2]
| 索引 | ratings[i] | 序列类型 | 当前糖果数 | 总糖果数 | 说明 |
|---|---|---|---|---|---|
| 0 | 1 | 初始 | 1 | 1 | 第一个孩子,至少有1个糖果 |
| 1 | 0 | 下降 | 1 | 2 | 评分下降,当前孩子1个糖果 |
| 2 | 2 | 上升 | 2 | 4 | 评分上升,当前孩子2个糖果 |
注意:这种方法的实现较为复杂,上面的分析是简化版,实际实现需要考虑更多细节。
代码实现
C# 实现
public class Solution {
public int Candy(int[] ratings) {
int n = ratings.Length;
int[] candies = new int[n];
// 初始化每个孩子至少有1个糖果
for (int i = 0; i < n; i++) {
candies[i] = 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 totalCandies = 0;
for (int i = 0; i < n; i++) {
totalCandies += candies[i];
}
return totalCandies;
}
}
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)
C++ 实现
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
vector<int> candies(n, 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] = max(candies[i], candies[i + 1] + 1);
}
}
// 计算总糖果数
int totalCandies = 0;
for (int i = 0; i < n; i++) {
totalCandies += candies[i];
}
return totalCandies;
}
};
执行结果
C# 实现
- 执行用时:96 ms
- 内存消耗:42.8 MB
Python 实现
- 执行用时:160 ms
- 内存消耗:17.8 MB
C++ 实现
- 执行用时:16 ms
- 内存消耗:17.4 MB
性能对比
| 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|
| C# | 96 ms | 42.8 MB | 执行速度适中,内存消耗较高 |
| Python | 160 ms | 17.8 MB | 执行速度较慢,内存消耗适中 |
| C++ | 16 ms | 17.4 MB | 执行速度最快,内存消耗最低 |
代码亮点
- 🎯 两次遍历解决问题,时间复杂度为O(n)
- 💡 巧妙利用从左到右和从右到左两次遍历,确保同时满足两个方向的要求
- 🔍 使用Math.Max函数确保取两次遍历结果的最大值
- 🎨 代码简洁清晰,逻辑易于理解
常见错误分析
- 🚫 只考虑从左到右的遍历,忽略了从右到左的情况
- 🚫 没有正确处理评分相等的情况
- 🚫 初始化糖果数为0,而不是1
- 🚫 在第二次遍历时,直接覆盖第一次遍历的结果,而不是取最大值
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 两次遍历 | O(n) | O(n) | 思路清晰,实现简单 | 需要额外空间存储每个孩子的糖果数 |
| 常数空间的一次遍历 | O(n) | O(1) | 空间复杂度低 | 实现复杂,不直观 |
| 贪心算法(两次遍历的变种) | O(n) | O(n) | 思路清晰,实现简单 | 需要额外空间存储每个孩子的糖果数 |
相关题目
- LeetCode 330. 按要求补齐数组 - 困难
- LeetCode 455. 分发饼干 - 简单
- LeetCode 860. 柠檬水找零 - 简单
- LeetCode 1046. 最后一块石头的重量 - 简单
- LeetCode 1642. 可以到达的最远建筑 - 中等