15.分发糖果
题目
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻两个孩子评分更高的孩子会获得更多的糖果。
- 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
- 输入:ratings = [1,0,2]
- 输出:5
- 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
- 输入:ratings = [1,2,2]
- 输出:4
- 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
提示:
- n == ratings.length
- 1 <= n <= 2 * 104
- 0 <= ratings[i] <= 2 * 104
思路
注意题目要求的是最少的糖果数目,且每个孩子至少分配1个糖果。
我们默认把所有小孩的糖果都是1。
那么本题我采用了两次贪心的策略:
- 第一次从左到右遍历,如果当前孩子评分高于左边孩子,那么当前孩子比左边多一个糖果;否则不处理,只保留一个糖果。
- 左规则:当
ratings[i−1]<ratings[i]时,i号学生的糖果数量将比i−1号孩子的糖果数量多。我们让left[i]=left[i−1]+1
- 左规则:当
- 第二次从右到左遍历,如果当前孩子评分高于右边孩子,那么取 “当前值” 与 “右侧糖果 +1” 中的最大值;否则不予处理。
- 右规则:当
ratings[i]>ratings[i+1]时,i号学生的糖果数量将比i+1号孩子的糖果数量多。
- 右规则:当
我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。
在实际代码中,我们先计算出左规则left数组,在计算右规则的时候只需要用单个变量记录当前位置的右规则,同时计算答案即可。
class Solution {
fun candy(ratings: IntArray): Int {
var count = 0
val length = ratings.size
val candyArray = IntArray(length) { 1 }
// 左规则: 从左向右,右边比左边高则加1
for (i in 1 until length) {
if (ratings[i] > ratings[i - 1]) {
candyArray[i] = candyArray[i - 1] + 1
}
}
count += candyArray[length - 1]
// 右规则: 从右向左,左边比右边高则去最大值
for (i in length - 2 downTo 0) {
if (ratings[i] > ratings[i + 1]) {
candyArray[i] = maxOf(candyArray[i], candyArray[i + 1] + 1)
}
count += candyArray[i]
}
return count
}
}
class Solution:
def candy(self, ratings: List[int]) -> int:
n = len(ratings)
candies = [1] * n # 每个孩子至少一个糖果
# 从左到右:右边评分高,则右边糖果 = 左边糖果 + 1
for i in range(1, n):
if ratings[i] > ratings[i - 1]:
candies[i] = candies[i - 1] + 1
# 从右到左:左边评分高,则左边糖果 = max(当前左边糖果, 右边糖果 + 1)
for i in range(n - 2, -1, -1):
if ratings[i] > ratings[i + 1]:
candies[i] = max(candies[i], candies[i + 1] + 1)
return sum(candies) # 返回总糖果数
复杂度分析:
-
时间复杂度:O(n),其中 n 是孩子的数量。我们需要遍历两次数组以分别计算满足左规则或右规则的最少糖果数量。
-
空间复杂度:O(n),其中 n 是孩子的数量。我们需要保存所有的左规则对应的糖果数量。
方法二: 一次遍历
在上面的方法中,我们只考虑了递增段的处理,没有考虑如何处理递减段,所以才必须使用两次遍历,那该如何将递减段的处理一同加入呢?
分析一下,如果当前孩子的ratings[i]比左侧孩子的ratings[i-1]更小,到底该分配多大的值?这取决于这个递减段的长度。
- 显然,如果i是最右侧的孩子,他可以只分配1颗糖果;
- 如果i是倒数第二个孩子,右侧还有一个递减,那么他最少分配2颗糖果。
核心思路:遍历数组时,跟踪当前是上升趋势、下降趋势,还是平;对于连续下降段,不立即分糖果,而是等下降结束后一次性累加。 观察下降的规律,因为不处理的话就是默认分配1。如果下降长度为k,则需要补上1+2+....+k,即k * (k + 1) / 2颗糖果。
规律:下降长度为 k 时,需要补的量是 (k-1) + (k-2) + ... + 1 + 0 = k*(k-1)/2。
所以正确公式是 k*(k-1)/2,不是 k*(k+1)/2。
加上默认的每人1颗(共 k 颗),下降段总糖果 = k + k*(k-1)/2 = k*(k+1)/2。
举个例子,假设ratings=[1,3,4,1],从左往右处理时,得到cadies=[1,2,3]。我们发现,下降段是[4,1],长度为1,所以补上1颗糖果,将数组变为 candies=[1,2,3,1]。
特别的,如果下降长度 >= 上升长度,需要为“山峰”孩子多加一个糖果。
举个例子,假设ratings=[1,2,4,3,2,1],从左往右处理时,得到cadies=[1,2,3]。
我们发现,下降段的长度为3,比上升段还长。
如果仅按照之前的处理,得到candies=[1,2,3,3,2,1]。实际上,第一个3应该变为4,也即山峰更高才对,正确的结果是candies=[1,2,4,3,2,1]。
代码如下:
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int total = 1; // 第一个孩子至少一个糖果
int up = 0; // 上升序列长度
int down = 0; // 下降序列长度
int peak = 0; // 当前上升段的长度峰值
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) { // 上升
up++;
peak = up;
down = 0;
total += 1 + up; // 当前孩子比前一个多一颗糖果
} else if (ratings[i] == ratings[i - 1]) { // 平
up = down = peak = 0;
total += 1; // 持平,默认为1
} else { // 下降
up = 0;
down++;
// 如果下降长度超过了之前的上升峰值,需要额外补偿
total += 1 + down - (peak >= down ? 1 : 0);
}
}
return total;
}
}
class Solution {
fun candy(ratings: IntArray): Int {
val length = ratings.size
// 第一个孩子最少是1颗
var total = 1
// 当前上升段的长度
var up = 0
// 当前下降段的长度
var down = 0
// 进入下降前的上升段长度(山峰高度)
var peak = 0
for (i in 1 until length) {
if (ratings[i] > ratings[i - 1]) {
// 上升: 当前孩子比左边多1颗,up + 1就是多出的量
up ++
down = 0
peak = up
total += (up + 1)
} else if (ratings[i] < ratings[i - 1]) {
// 下降: down是当前下降段的长度
// 每新增一个下降节点,新增量为down
down ++
up = 0
// peak >= down: 山峰够高,加 down(不补偿)
// peak < down: 山峰不够高,加 down+1(补偿山峰)
if (peak >= down) {
total += down
} else {
total += down + 1
}
} else {
// 平坡: 两侧互不影响,重置状态,当前孩子分1颗
up = 0
down = 0
peak = 0
total += 1
}
}
return total
}
}
- 时间复杂度: O(n),其中 n为孩子总数
-
空间复杂度: O(1),仅使用常数个额外变量
- 邮箱 :charon.chui@gmail.com
- Good Luck!