每日一算:分发糖果
在算法面试中,“分发糖果” 是一道经典的贪心算法应用题,核心考察对 “局部最优推导全局最优” 的理解。本文将从问题分析出发,提供两种主流解题思路,并附上 C++ 实现代码,帮助你彻底掌握这道题。
一、问题重述
题目要求
有 n
个孩子站成一排,每个孩子有评分 ratings[i]
,分发糖果需满足:
- 每个孩子至少得 1 颗糖果;
- 相邻孩子中,评分高的孩子必须获得更多糖果。
求最少需要准备的糖果总数。
示例理解
- 示例 1:
ratings = [1,0,2]
分发方案[2,1,2]
,总数5
。
解释:第二个孩子评分最低(0)得 1 颗,第一个比第二个高(1>0)得 2 颗,第三个比第二个高(2>0)得 2 颗。 - 示例 2:
ratings = [1,2,2]
分发方案[1,2,1]
,总数4
。
解释:第三个孩子与第二个评分相同(2=2),无需更多糖果,故得 1 颗(满足 “至少 1 颗” 即可)。
二、解题思路
核心难点
相邻关系是 “双向约束”:既要保证 “左→右” 方向评分高的糖果多,也要保证 “右→左” 方向评分高的糖果多。若只单向处理,会导致另一侧约束被破坏。
思路 1:两次遍历(贪心经典解法)
原理
贪心算法的核心是 “分阶段满足约束”:
- 左→右遍历:保证每个孩子比左边评分高时,糖果数比左边多(不考虑右边);
- 右→左遍历:保证每个孩子比右边评分高时,糖果数比右边多(此时需结合左→右的结果,取最大值,避免破坏左侧约束)。
步骤拆解
- 初始化数组
candies
,所有元素为 1(满足 “至少 1 颗”); - 左→右遍历(处理 “左边约束”):
若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)
; - 求和
candies
数组,得到最少糖果总数。
示例验证(以 ratings = [1,0,2]
为例)
- 初始化:
candies = [1,1,1]
- 左→右遍历:
i=1(0 <1):无变化;i=2(2> 0):candies[2] = 1+1=2
→ 此时[1,1,2]
- 右→左遍历:
i=1(0 <2):无变化;i=0(1> 0):candies[0] = max(1, 1+1)=2
→ 最终[2,1,2]
- 求和:2+1+2=5(正确)
思路 2:一次遍历(优化空间,进阶解法)
原理
观察到 “糖果数的变化与评分的递增 / 递减序列相关”,可通过记录当前递增 / 递减序列的长度,动态计算糖果数,无需额外存储 candies
数组(空间复杂度从 O (n) 降至 O (1))。
关键概念
up
:当前递增序列的长度(评分持续上升时,每多一个孩子,糖果数 + 1);down
:当前递减序列的长度(评分持续下降时,每多一个孩子,糖果数需回溯调整,避免重复计算);pre
:前一个孩子的糖果数(动态更新)。
步骤拆解
- 初始化
result=1
(第一个孩子至少 1 颗)、up=1
、down=0
、pre=1
; - 从第二个孩子开始遍历:
- 若
ratings[i] > ratings[i-1]
(递增):up = pre + 1
,down=0
,pre=up
,result += up
; - 若
ratings[i] == ratings[i-1]
(相等):up=1
,down=0
,pre=1
,result += 1
(相等时无需更多糖果,取 1 即可); - 若
ratings[i] < ratings[i-1]
(递减):down += 1
,up=1
,
若down == pre
(递减序列长度等于前一个糖果数,需补 1 避免冲突):result += 1
,result += down
,pre=1
(递减序列中当前孩子糖果数为 1,前一个需回溯调整);
- 若
- 遍历结束,
result
即为最少糖果总数。
示例验证(以 ratings = [1,2,2]
为例)
- 初始:
result=1
,up=1
,down=0
,pre=1
- i=1(2>1,递增):
up=2
,pre=2
,result=1+2=3
- i=2(2=2,相等):
pre=1
,result=3+1=4
(正确)
三、C++ 代码实现
实现 1:两次遍历(易理解,推荐面试首选)
#include <iostream>
#include <vector>
#include <algorithm> // for max()using namespace std;class Solution {
public:int candy(vector<int>& ratings) {int n = ratings.size();if (n == 0) return 0;// 初始化:每个孩子至少1颗糖果vector<int> candies(n, 1);// 左→右遍历:处理左边约束(比左边高则+1)for (int i = 1; i < n; ++i) {if (ratings[i] > ratings[i-1]) {candies[i] = candies[i-1] + 1;}}// 右→左遍历:处理右边约束(比右边高则取max(当前, 右边+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 total = 0;for (int num : candies) {total += num;}return total;}
};// 测试代码
int main() {Solution sol;vector<int> ratings1 = {1,0,2};cout << "示例1最少糖果数:" << sol.candy(ratings1) << endl; // 输出5vector<int> ratings2 = {1,2,2};cout << "示例2最少糖果数:" << sol.candy(ratings2) << endl; // 输出4return 0;
}
实现 2:一次遍历(空间优化,进阶)
#include <iostream>
#include <vector>using namespace std;class Solution {
public:int candy(vector<int>& ratings) {int n = ratings.size();if (n == 0) return 0;if (n == 1) return 1;int result = 1; // 第一个孩子的1颗糖果int up = 1; // 当前递增序列长度int down = 0; // 当前递减序列长度int pre = 1; // 前一个孩子的糖果数for (int i = 1; i < n; ++i) {if (ratings[i] > ratings[i-1]) {// 递增序列up = pre + 1;down = 0;pre = up;result += up;} else if (ratings[i] == ratings[i-1]) {// 相等:当前孩子取1颗,重置状态up = 1;down = 0;pre = 1;result += 1;} else {// 递减序列down += 1;up = 1;// 若递减长度等于前一个糖果数,需补1(避免冲突)if (down == pre) {down += 1;}result += down;pre = 1; // 递减序列中当前孩子糖果数为1}}return result;}
};// 测试代码
int main() {Solution sol;vector<int> ratings1 = {1,0,2};cout << "示例1最少糖果数:" << sol.candy(ratings1) << endl; // 输出5vector<int> ratings2 = {1,2,2};cout << "示例2最少糖果数:" << sol.candy(ratings2) << endl; // 输出4return 0;
}
四、复杂度分析
解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
两次遍历 | O(n) | O(n) | 面试首选,易理解、易调试 |
一次遍历 | O(n) | O(1) | 空间敏感场景(如大数据量) |
五、总结
- 两次遍历解法的核心是 “分阶段满足双向约束”,通过两次单向遍历覆盖所有相邻关系,逻辑清晰,适合面试中快速实现;
- 一次遍历解法通过动态记录序列长度优化空间,需要对 “递减序列的回溯调整” 有深入理解,适合进阶学习;
- 无论哪种解法,都遵循贪心算法的 “局部最优→全局最优” 思想:每次只处理当前能确定的最优解,最终累积得到全局最少糖果数。
建议先掌握两次遍历解法,再尝试理解一次遍历的优化逻辑,逐步提升对贪心算法的应用能力。