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),但理解难度较高。
在大多数情况下,解法一和解法二足够高效且易于理解。解法四虽然在空间复杂度上最优,但代码不够直观,更难维护。在实际应用中,应根据具体需求选择合适的解法。