1. 问题背景与核心挑战
糖果分配问题看似简单,实则暗藏玄机。想象你是一位班主任,要给排成一列的孩子们分发糖果,既要满足"成绩好的孩子要比邻座拿更多糖"的基本规则,又要考虑班级经费有限需要最小化糖果总量。这就是LeetCode 135题给我们出的难题。
这个问题的难点在于约束条件的双向性——每个孩子的糖果数需要同时满足与左右邻居的比较关系。当遇到连续递增或递减的分数序列时,简单的单向遍历就会失效。我在第一次尝试时,就因为没有考虑双向约束,导致提交的解法在测试用例[1,3,4,2,1]上翻了车。
2. 解题思路拆解
2.1 暴力法的局限
最直观的想法是暴力枚举所有可能的分配方案,检查是否满足条件后取最小值。但这种方法的时间复杂度是O(n^2),当n=10^5时完全不可行。我在本地测试时,仅n=20的case就需要近10秒才能跑完。
2.2 关键突破:双向遍历
经过多次尝试,我发现可以将问题分解为两个单向约束:
- 从左到右:保证右边分数高的孩子糖果更多
- 从右到左:保证左边分数高的孩子糖果更多
最终每个孩子的糖果数取这两个方向计算结果的较大值。这种方法将时间复杂度降到了O(n),空间复杂度O(n),完美满足题目要求。
3. 算法实现详解
3.1 初始化阶段
python复制def candy(ratings):
n = len(ratings)
left = [1] * n # 从左到右的分配
right = [1] * n # 从右到左的分配
每个孩子至少分到1颗糖,这是题目隐含的最小单位。在实际编码时,我最初忽略了这一点,导致边界条件处理出错。
3.2 左遍历处理
python复制for i in range(1, n):
if ratings[i] > ratings[i-1]:
left[i] = left[i-1] + 1
这里有个易错点:只有当右边分数严格大于左边时才增加糖果数。我第一次写成了>=,导致分数相等的孩子也被多分了糖,违反了最小化原则。
3.3 右遍历处理
python复制for i in range(n-2, -1, -1):
if ratings[i] > ratings[i+1]:
right[i] = right[i+1] + 1
反向遍历时要注意索引范围。我曾在面试中见到候选人写成range(n-1, -1, -1),这会导致数组越界。正确的写法是从倒数第二个元素开始。
3.4 结果合并
python复制total = 0
for i in range(n):
total += max(left[i], right[i])
return total
合并阶段取两个方向的最大值,确保同时满足左右约束。这里有个优化点:可以不用显式存储right数组,直接在遍历时计算并累加最大值。
4. 优化技巧与边界处理
4.1 空间优化版
python复制def candy(ratings):
n = len(ratings)
candies = [1] * n
# 左到右
for i in range(1, n):
if ratings[i] > ratings[i-1]:
candies[i] = candies[i-1] + 1
# 右到左并累加
total = candies[-1]
for i in range(n-2, -1, -1):
if ratings[i] > ratings[i+1]:
candies[i] = max(candies[i], candies[i+1] + 1)
total += candies[i]
return total
这个版本将空间复杂度优化到O(1),但会修改原数组。在面试中建议先讲清楚基础解法,再提出优化方案。
4.2 特殊case处理
- 空数组:返回0
- 单元素数组:返回1
- 所有分数相同:返回n(每人1颗)
- 严格递增序列:返回n*(n+1)/2(等差数列求和)
- 严格递减序列:同上
我在白板编码时特意准备了这些测试用例,向面试官展示全面的思考过程。
5. 数学原理与复杂度分析
这个问题本质上是在寻找满足特定约束条件下的最小解。从数学角度看,它属于约束优化问题,其中:
- 目标函数:minimize Σc_i
- 约束条件:c_i > c_j if rating_i > rating_j (j = i±1)
贪心算法在这里能奏效,是因为约束条件具有局部性——每个孩子的糖果数只与直接邻居相关。这使得我们可以通过两次线性扫描就找到全局最优解。
时间复杂度:O(n) 两次遍历
空间复杂度:O(n) 存储中间结果(可优化到O(1))
6. 常见错误与调试技巧
6.1 典型错误案例
- 单向遍历:只考虑左边或右边的约束
- 等号处理:错误地在
==时也增加糖果 - 初始化错误:忘记每人至少1颗糖
- 索引越界:反向遍历时范围设置不当
6.2 调试建议
- 先用小测试用例手动模拟(如[1,0,2])
- 打印中间数组(left/right)检查是否符合预期
- 特别注意序列的峰谷位置(如[1,3,4,2,1]中的4是峰值)
7. 变种问题与扩展思考
7.1 环形排列
如果孩子们围坐一圈怎么办?这时首尾也形成约束。解决方法:
- 拆环为链,处理两次
- 比较首尾元素,必要时调整
7.2 多维扩展
如果是二维矩阵中的分配(每个位置与上下左右比较),问题会变得复杂得多,可能需要使用优先级队列或动态规划。
7.3 实际应用场景
这类分配问题在资源调度、任务分配等领域都有应用。比如:
- 服务器负载均衡
- 工人任务分配
- 带宽分配算法
8. 编码风格与面试技巧
在面试中遇到这类问题时,建议采取以下步骤:
- 明确问题:复述题目要求,确认理解正确
- 举例说明:用具体例子演示输入输出
- 提出暴力解:分析其缺点
- 优化思路:解释双向遍历的原理
- 代码实现:边写边解释关键点
- 测试验证:用多个case测试代码
- 复杂度分析:说明时间/空间复杂度
- 优化讨论:提出可能的改进空间
我在面试候选人时,特别看重他们能否清晰地解释算法原理,而不仅仅是写出正确代码。双向遍历的思路体现了对问题本质的深刻理解,这正是面试官最希望看到的。