LeetCode 135. Candy:从直觉到最优解的完整推导

这篇文章记录我做 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 = 2cur_candy = 1,total = 2
  • i = 2:cur_rating = 0 < pre_rating = 1cur_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]

直觉上就会想到:

  1. 先从左到右扫描一遍,处理「左邻居」的约束。
  2. 再从右到左扫描一遍,处理「右邻居」的约束。

于是,一个自然的方案是:

  • 准备一个 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];
}

边界情况与复杂度分析

边界情况

  1. ratingsSize == 1

    • 唯一的孩子,至少 1 颗糖,答案为 1。[page:2]
  2. 全递增,例如 [1, 2, 3, 4]

    • 从左到右:[1, 2, 3, 4]
    • 从右到左不起作用,最终 [1, 2, 3, 4],总和 10。
  3. 全递减,例如 [4, 3, 2, 1]

    • 左到右:初始 [1,1,1,1],不会增长。
    • 右到左:调整成 [4,3,2,1],总和 10。
  4. 锯齿形,例如 [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,不动
    • 最终 [1,2,1],满足两侧约束。

这些例子都能正确处理,说明方案是全面的。

时间和空间复杂度

  • 时间复杂度:

    • 初始化 candies:O(n)
    • 左到右扫描:O(n)
    • 右到左扫描:O(n)
    • 求和:O(n)
    • 总计仍为 O(n)。[page:2]
  • 空间复杂度:

    • 使用了一个长度为 n 的数组 candies,额外空间 O(n)。[page:2]

在实际面试中,这个复杂度是完全可以接受的。


最终 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]

小结:从直觉到标准解法的思维路径

这道题很适合练「从错误直觉到正确解法」的思维过程:

  1. 先从直觉出发:一趟从左到右,保证「比左边高就多糖」。
  2. 发现问题:只满足左邻居,但忽略右邻居,举反例验证。
  3. 意识到需要双向约束:再从右到左扫一遍,补充右侧约束。
  4. 引入 candies 数组,保存每个孩子的分配,以便在两次扫描中调整。
  5. 用「取两边约束的最大值」的思想,写出最终解法。

这条路径本身就是一次完整的"面试中的思维展示",比直接背公式式解答要更有价值。

markdown 复制代码
如果你也在刷 Top Interview 150,这道题是一个很好的「相邻约束 + 两趟扫描」模板题,后面遇到类似模式可以直接类比。
相关推荐
WHS-_-20225 小时前
Tensor-Based Target Sensing for Resource-Irregular ISAC Systems
linux·人工智能·算法
成都易yisdong5 小时前
高程异常计算器:一款集成Geoid、重力场与地磁场的专业工具
算法
王老师青少年编程5 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【反悔贪心】:种树
c++·算法·贪心·反悔贪心·csp·信奥赛·种树
南宫萧幕5 小时前
基于 PSO 的 HEV 能量管理策略:从联合仿真建模到排错实战
开发语言·python·算法·matlab·控制
生物信息与育种5 小时前
全基因组重测序及群体遗传与进化分析技术服务指南
人工智能·深度学习·算法·数据分析·r语言
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题】【Java基础篇】第23题:ConcurrentHashMap的底层原理是什么
java·开发语言·算法·哈希算法·散列表·hash
葳_人生_蕤5 小时前
hot100——回溯和DFS、BFS
算法·深度优先
Eloudy5 小时前
Steane码的稳定子的生成元集计算过程
算法
MegaDataFlowers5 小时前
快速算法验证流水线
算法