LeetCode 135 题目介绍
LeetCode 135. Candy 是一道困难级别的数组问题:
有 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻两个孩子评分更高的孩子会获得更多的糖果。 请你计算并返回需要准备的 最少糖果数目 。
这个问题看似简单,但需要同时满足两个相邻位置的约束,因此有一定难度。下面将详细分析四种不同的解法。
解法一:两次遍历
第一种解法使用两次遍历方式,先从左向右遍历确保右侧评分更高的孩子获得更多糖果,再从右向左遍历确保左侧评分更高的孩子获得更多糖果。
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
Arrays.fill(candies,1);
for(int i = 1; i < n ; i++){
if(ratings[i] > ratings[i-1]){
candies[i] = Math.max(candies[i],candies[i-1] + 1);
}
}
for(int j = n - 2; j>=0; j--){
if(ratings[j] > ratings[j+1]){
candies[j] = Math.max(candies[j], candies[j+1] + 1);
}
}
int total = 0;
for(int count : candies){
total += count;
}
return total;
}
}
解法一详解
- 初始化一个长度为 n 的数组
candies,每个位置都为 1,表示每个孩子至少分配一个糖果。 - 第一次遍历(从左到右):
- 对于每个位置 i (i > 0),如果
ratings[i] > ratings[i-1],即当前孩子评分高于左边的孩子,则当前孩子应该比左边孩子多分配至少一个糖果。 - 此时,
candies[i] = Math.max(candies[i], candies[i-1] + 1),确保满足左边的约束。
- 对于每个位置 i (i > 0),如果
- 第二次遍历(从右到左):
- 对于每个位置 j (j < n-1),如果
ratings[j] > ratings[j+1],即当前孩子评分高于右边的孩子,则当前孩子应该比右边孩子多分配至少一个糖果。 - 此时,
candies[j] = Math.max(candies[j], candies[j+1] + 1),确保满足右边的约束。 - 使用 Math.max 是因为第一次遍历可能已经给当前位置分配了更多糖果。
- 对于每个位置 j (j < n-1),如果
- 最后统计总糖果数。
时间复杂度: O(n),两次遍历数组。
空间复杂度: O(n),需要一个额外数组存储每个孩子的糖果数。
解法二:左右两个数组法
第二种解法使用两个数组分别记录从左到右和从右到左的约束,最后取两者的最大值。
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] leftToRight = new int[n];
int[] rightToLeft = new int[n];
Arrays.fill(leftToRight,1);
Arrays.fill(rightToLeft,1);
for(int i = 1; i < n; i++){
if(ratings[i] > ratings[i-1]){
leftToRight[i] = leftToRight[i-1] + 1;
}
}
for(int i = n - 2; i >=0; i--){
if(ratings[i] > ratings[i+1]){
rightToLeft[i] = rightToLeft[i+1] + 1;
}
}
int total = 0;
for(int i = 0; i < n; i++){
total += Math.max(leftToRight[i],rightToLeft[i]);
}
return total;
}
}
解法二详解
- 创建两个长度为 n 的数组:
leftToRight:表示只考虑左侧约束时每个孩子应该获得的糖果数rightToLeft:表示只考虑右侧约束时每个孩子应该获得的糖果数
- 初始化两个数组的每个位置为 1。
- 从左到右遍历,更新
leftToRight数组:- 如果
ratings[i] > ratings[i-1],则leftToRight[i] = leftToRight[i-1] + 1
- 如果
- 从右到左遍历,更新
rightToLeft数组:- 如果
ratings[i] > ratings[i+1],则rightToLeft[i] = rightToLeft[i+1] + 1
- 如果
- 对于每个位置 i,最终分配的糖果数应该是
max(leftToRight[i], rightToLeft[i]),以同时满足左右两边的约束。 - 计算总和并返回。
相比解法一,这种方法逻辑更清晰,但使用了两个数组,空间复杂度为 O(2n)。时间复杂度仍为 O(n)。
解法三:优先队列法
第三种解法使用优先队列,按照评分从低到高的顺序处理每个孩子,确保低评分的孩子先被处理。
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
Arrays.fill(candies,1);
PriorityQueue<Integer> queue = new PriorityQueue<>((a,b)-> ratings[a] - ratings[b]);
for(int i = 0 ; i < n ; i++){
queue.offer(i);
}
for(int i = 0; i < n; i++){
int j = queue.poll();
if(j > 0 && ratings[j] > ratings[j-1]){
candies[j] = Math.max(candies[j], candies[j-1] + 1);
}
if(j < n-1 && ratings[j] > ratings[j+1]){
candies[j] = Math.max(candies[j],candies[j+1] + 1);
}
}
int total = 0;
for(int count : candies){
total += count;
}
return total;
}
}
解法三详解
- 初始化一个长度为 n 的数组
candies,每个位置都为 1。 - 创建一个优先队列,按照评分从低到高的顺序存储孩子的索引。
- 将所有孩子的索引加入优先队列。
- 按照评分从低到高的顺序依次处理每个孩子:
- 检查当前孩子与左右相邻孩子的评分关系
- 如果当前孩子评分高于左侧孩子,则
candies[j] = Math.max(candies[j], candies[j-1] + 1) - 如果当前孩子评分高于右侧孩子,则
candies[j] = Math.max(candies[j], candies[j+1] + 1)
- 计算总糖果数并返回。
这种方法的核心思想是:优先处理评分较低的孩子,因为他们对相邻高评分孩子的糖果数有影响。由于使用了优先队列,时间复杂度为 O(n log n),空间复杂度为 O(n)。
解法四:常数空间一次遍历法
第四种解法最为巧妙,只需要一次遍历且不需要额外数组,采用贪心的思想解决问题。
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int total = 1;
int up = 1;
int peak = 1;
int down = 0;
for(int i = 1; i < n; i++){
if(ratings[i] > ratings[i-1]){
up++;
peak = up;
down = 0;
total += up;
}else if(ratings[i] < ratings[i-1]){
down++;
up = 1;
total += down + (down >= peak ? 1 : 0);
}else{
up = 1;
down = 0;
peak = 1;
total +=1;
}
}
return total;
}
}
解法四详解
这种方法通过一次遍历和几个辅助变量来计算总糖果数,避免了额外数组的开销。其核心思想是追踪递增和递减序列的长度,并据此更新糖果总数。
变量说明:
total: 已分配的糖果总数。初始化为 1,因为第一个孩子至少有 1 个糖果。up: 当前连续递增序列的长度。如果ratings[i] > ratings[i-1],则up递增。up的值也代表当前孩子在递增序列末端应得的糖果数。down: 当前连续递减序列的长度。如果ratings[i] < ratings[i-1],则down递增。peak: 最近一个递增序列的峰值(即峰顶孩子获得的糖果数)。当进入递减序列时,peak的值用于确保峰顶孩子的糖果数满足其两边的约束。
算法步骤与示例
我们以 ratings = [1, 2, 8, 7, 6, 5] 为例,逐步解释算法流程。
初始状态: total = 1 (为 ratings[0]=1 分配1颗糖果), up = 1, peak = 1, down = 0。
循环从 i = 1 开始。
i |
ratings[i-1] |
ratings[i] |
条件判断 | up |
peak |
down |
total 更新逻辑 |
total |
注释 (隐含的糖果分配思路) |
|---|---|---|---|---|---|---|---|---|---|
| 0 | - | 1 | 初始 | 1 | 1 | 0 | total = 1 (为 ratings[0]) |
1 | c[0]=1 |
| 1 | 1 | 2 | ratings[i] > ratings[i-1] |
2 | 2 | 0 | up=2, peak=up=2, down=0. total += up (1+2) |
3 | ratings[1] 在递增序列中,获得 up=2 颗糖果。c[1]=2. 当前糖果 [1,2] |
| 2 | 2 | 8 | ratings[i] > ratings[i-1] |
3 | 3 | 0 | up=3, peak=up=3, down=0. total += up (3+3) |
6 | ratings[2] 在递增序列中,获得 up=3 颗糖果。c[2]=3. 当前糖果 [1,2,3] |
| 3 | 8 | 7 | ratings[i] < ratings[i-1] |
1 | 3 | 1 | down=1, up=1. total += down (6+1). down < peak (1<3), 不额外加。 |
7 | ratings[3] 在递减序列中,获得1颗。total增加1。c[3]=1. 当前糖果 [1,2,3,1] |
| 4 | 7 | 6 | ratings[i] < ratings[i-1] |
1 | 3 | 2 | down=2, up=1. total += down (7+2). down < peak (2<3), 不额外加。 |
9 | ratings[4] 获得1颗。ratings[3]因处于长度为2的下坡,糖果数需调整为2。total增加2 (1为c[4], 1为调整c[3])。当前糖果 [1,2,3,2,1] |
| 5 | 6 | 5 | ratings[i] < ratings[i-1] |
1 | 3 | 3 | down=3, up=1. total += down (9+3). down >= peak (3>=3), total += 1 (12+1). |
13 | ratings[5] 获得1颗。ratings[4]调整为2, ratings[3]调整为3。total增加3。因down>=peak,峰顶ratings[2]糖果+1。当前糖果 [1,2,4,3,2,1] |
最终 total = 13。
对应的糖果分配为 [1, 2, 4, 3, 2, 1],满足所有条件。
代码逻辑详解
if (ratings[i] > ratings[i-1])(递增情况):up++: 延长当前递增序列。up代表当前孩子应得的糖果数。peak = up: 更新峰值为当前up值。down = 0: 重置递减序列计数器。total += up: 将当前孩子应得的up颗糖果计入总数。
else if (ratings[i] < ratings[i-1])(递减情况):down++: 延长当前递减序列。up = 1: 递增序列中断,如果后续再次递增,新的up将从1开始。total += down: 这是关键之一。当遇到第down个递减元素时:- 当前孩子
ratings[i]获得 1 颗糖果。 - 递减序列中的前
down-1个孩子,它们各自的糖果数需要比其后的孩子多1。total += down实际上是为当前孩子加1颗糖,并为前面down-1个递减序列中的孩子各补1颗糖,以维持递减关系(例如,使它们的糖果数从down-1, down-2, ..., 1变为down, down-1, ..., 2,再加上当前孩子的1颗)。
- 当前孩子
total += (down >= peak ? 1 : 0): 这是关键之二。peak记录的是上一个递增序列末尾(即山峰)的糖果数。- 如果当前的递减序列长度
down大于或等于peak,意味着仅仅按递减序列分配糖果(例如peak, down, down-1, ..., 1)可能会导致山峰处的糖果数不大于其后第一个递减孩子的糖果数(即peak不大于down)。 - 为了满足
ratings[peak_idx] > ratings[peak_idx+1]=>candies[peak_idx] > candies[peak_idx+1],山峰peak处的糖果数可能需要增加。 - 每当
down >= peak时,给total额外加 1。这相当于给山峰位置的孩子增加一颗糖果,以确保它比后续递减序列的第一个孩子糖果多。这个调整会随着down的增长持续进行,只要down >= peak。
else(即ratings[i] == ratings[i-1], 相等情况):up = 1,down = 0,peak = 1: 重置所有计数器。当前孩子获得1颗糖果,并形成一个新的潜在峰值1。total += 1: 将当前孩子的1颗糖果计入总数。
这种方法通过巧妙地维护 up, down, peak 状态,并在一次遍历中正确累计总糖果数,尤其是处理递减序列对前面山峰的影响,从而达到 O(1) 空间复杂度的优化。
时间复杂度: O(n),只需一次遍历。 空间复杂度: O(1),只使用几个变量,不需要额外数组。
总结
- 解法一(两次遍历):思路清晰,实现简单,时间 O(n),空间 O(n)。
- 解法二(两个数组):与解法一类似,但逻辑更清晰,时间 O(n),空间 O(2n)。
- 解法三(优先队列):利用优先队列按评分从低到高处理,时间 O(n log n),空间 O(n)。
- 解法四(常数空间一次遍历):最优解,时间 O(n),空间 O(1),但理解难度较高。
在大多数情况下,解法一和解法二足够高效且易于理解。解法四虽然在空间复杂度上最优,但代码不够直观,更难维护。在实际应用中,应根据具体需求选择合适的解法。