这篇文章记录我做 LeetCode 135「Candy」这题时的完整思考过程:
从一开始只看一边邻居的错误思路,到最后形成标准的双向扫描 O(n) 解法,并给出 C 语言实现。
题目链接:Candy - LeetCode[page:2]
题目理解与直觉解法
题目大意:
- 有 n 个孩子排成一行,每个孩子有一个评分
ratings[i]。[page:2] - 分糖果规则:
- 每个孩子至少 1 颗糖。[page:2]
- 评分更高的孩子,比相邻的孩子分到更多糖。[page:2]
- 目标:在满足规则的前提下,使糖果总数最少,返回这个最少总数。[page:2]
一个很自然的直觉是:从左到右扫描一遍,保证「比左边评分高,就比左边多给一颗糖」。
伪代码大概是:
c
pre_rating = 0;
pre_candy = 0;
total_candy = 0;
for i in [0..n-1]:
cur_rating = ratings[i];
cur_candy = 1;
if i > 0 && cur_rating > pre_rating:
cur_candy = pre_candy + 1;
total_candy += cur_candy;
pre_rating = cur_rating;
pre_candy = cur_candy;
看起来满足了「高分的比左边多」,而且是 O(n) 时间、O(1) 额外空间。
问题在于:只管了左边邻居,完全没有看右边邻居。
只看一侧的反例与问题暴露
考虑这个例子:
text
ratings =[1][2]
用上面的一趟算法:
- i = 0:
cur_rating = 2,左边没有人 →cur_candy = 1,total = 1 - i = 1:
cur_rating = 1 < pre_rating = 2→cur_candy = 1,total = 2 - i = 2:
cur_rating = 0 < pre_rating = 1→cur_candy = 1,total = 3
得到分配 [1, 1, 1]。
但显然不满足:
- 第 0 个孩子评分 2,比第 1 个评分 1 高,却没有比 TA 多糖。
问题的本质:
- 题目要求的是「每个孩子相对于两个邻居都要满足条件」;
- 一趟从左到右,只能保证「对左边邻居正确」,对右边邻居没有约束。
所以,只做一边的单向扫描是不够的。
引入双向约束的思考:右向也要看一遍
我们希望同时满足:
- 从左看:
ratings[i] > ratings[i-1]⇒candies[i] > candies[i-1] - 从右看:
ratings[i] > ratings[i+1]⇒candies[i] > candies[i+1]
直觉上就会想到:
- 先从左到右扫描一遍,处理「左邻居」的约束。
- 再从右到左扫描一遍,处理「右邻居」的约束。
于是,一个自然的方案是:
- 准备一个
candies数组,长度为 n,初始全部为 1(满足「至少 1 颗糖」)。 - 第一遍,从左到右:
- 如果
ratings[i] > ratings[i-1],就令candies[i] = candies[i-1] + 1。
- 如果
- 第二遍,从右到左:
- 如果
ratings[i] > ratings[i+1],就保证candies[i] > candies[i+1]。
- 如果
关键问题变成:
第二遍更新
candies[i]的时候,如何在满足右边约束的同时,不破坏第一遍已经满足的左边约束,而且不多给糖?
完整算法设计:两趟扫描 + 取最大值思想
第一步:初始化
- 分配一个数组
candies[n]。 - 所有元素初始化为 1,代表每个孩子至少一颗糖。
c
for (i = 0; i < ratingsSize; i++) {
candies[i] = 1;
}
第二步:从左到右,处理左邻居约束
- 对 i 从 1 到 n-1:
- 如果
ratings[i] > ratings[i-1],则candies[i] = candies[i-1] + 1。
- 如果
c
for (i = 1; i < ratingsSize; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}
这一步保证了:对所有 i,如果它比分数更高的左邻居,就比左邻居多糖。
第三步:从右到左,处理右邻居约束
现在处理右邻居:
- 对 i 从 n-2 到 0:
- 如果
ratings[i] > ratings[i+1],我们希望candies[i] > candies[i+1]。 - 但
candies[i]可能已经因为左邻居在第一遍被设置成一个更大的值了。 - 所以正确的做法是:取两种约束下需要的最大值。
- 如果
在代码里,这对应为:
c
for (i = ratingsSize - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1] &&
candies[i] <= candies[i + 1]) {
candies[i] = candies[i + 1] + 1;
}
}
这里的关键条件:
ratings[i] > ratings[i + 1]:说明 i 这个孩子比分数更高,必须糖更多。candies[i] <= candies[i + 1]:- 如果当前
candies[i]已经比candies[i+1]大 1 或更多,就不需要再增加; - 如果不够,就把它提升到
candies[i+1] + 1。
- 如果当前
理解成「左约束给了一个下限,右约束又给了一个下限,取两者中的最大值」。
第四步:累加总糖果数
- 最后再遍历一遍
candies数组,把所有值加起来,就是最少需要的糖果数。
c
int total_candies = 0;
for (i = 0; i < ratingsSize; i++) {
total_candies += candies[i];
}
边界情况与复杂度分析
边界情况
-
ratingsSize == 1- 唯一的孩子,至少 1 颗糖,答案为 1。[page:2]
-
全递增,例如
[1, 2, 3, 4]- 从左到右:
[1, 2, 3, 4] - 从右到左不起作用,最终
[1, 2, 3, 4],总和 10。
- 从左到右:
-
全递减,例如
[4, 3, 2, 1]- 左到右:初始
[1,1,1,1],不会增长。 - 右到左:调整成
[4,3,2,1],总和 10。
- 左到右:初始
-
锯齿形,例如
[1, 3, 2]- 左到右:
- i=0:1
- i=1:比左边高 → 2
- i=2:比左边低 → 1
- 暂时
[1,2,1]
- 右到左:
- i=1:
ratings[1]=3 > ratings[2]=2,且candies[1]=2 > candies[2]=1,无需调整 - i=0:1 不大于 3,不动
- i=1:
- 最终
[1,2,1],满足两侧约束。
- 左到右:
这些例子都能正确处理,说明方案是全面的。
时间和空间复杂度
-
时间复杂度:
- 初始化 candies:O(n)
- 左到右扫描:O(n)
- 右到左扫描:O(n)
- 求和:O(n)
- 总计仍为 O(n)。[page:2]
-
空间复杂度:
- 使用了一个长度为 n 的数组
candies,额外空间 O(n)。[page:2]
- 使用了一个长度为 n 的数组
在实际面试中,这个复杂度是完全可以接受的。
最终 C 语言实现
下面是最终的 C 实现代码(已在 LeetCode 提交通过,50/50 测试用例,0ms):[page:2]
c
int candy(int* ratings, int ratingsSize) {
int i, total_candies = 0;
if (ratings == NULL || ratingsSize == 0)
return 0;
if (ratingsSize == 1)
return 1;
int *candies = (int *)malloc(ratingsSize * sizeof(int));
if (candies == NULL)
return 0;
for (i = 0; i < ratingsSize; i++) {
candies[i] = 1;
}
// left to right
for (i = 1; i < ratingsSize; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}
// right to left
for (i = ratingsSize - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1] &&
candies[i] <= candies[i + 1]) {
candies[i] = candies[i + 1] + 1;
}
}
for (i = 0; i < ratingsSize; i++) {
total_candies += candies[i];
}
free(candies);
return total_candies;
}
这份代码的特点:
- 逻辑清晰,对应上文的四个步骤。
- 边界情况处理完整(
ratingsSize == 0/1)。 - 内存分配后有
NULL检查,并在结尾free掉。 - 满足题目要求的 O(n) 时间复杂度,O(n) 额外空间复杂度。[page:2]
小结:从直觉到标准解法的思维路径
这道题很适合练「从错误直觉到正确解法」的思维过程:
- 先从直觉出发:一趟从左到右,保证「比左边高就多糖」。
- 发现问题:只满足左邻居,但忽略右邻居,举反例验证。
- 意识到需要双向约束:再从右到左扫一遍,补充右侧约束。
- 引入
candies数组,保存每个孩子的分配,以便在两次扫描中调整。 - 用「取两边约束的最大值」的思想,写出最终解法。
这条路径本身就是一次完整的"面试中的思维展示",比直接背公式式解答要更有价值。
markdown
如果你也在刷 Top Interview 150,这道题是一个很好的「相邻约束 + 两趟扫描」模板题,后面遇到类似模式可以直接类比。