每日一题:分发糖果问题详解
简介
在今天的每日一题中,我们将探讨一个经典的算法问题——分发糖果。这个问题不仅考察了我们对数组操作的理解,还涉及到了贪心算法的应用。通过这个问题,我们可以学习如何在满足特定条件的情况下,最小化资源的使用。此题的关键在于如何在保证右边分高的同学比左边糖果多的同时,左边分高的同学能保持比右边糖果多。
1、题目描述
有N个小朋友站成一排,每个小朋友都有一个评分。你需要按照以下规则给孩子们分糖果:
- 每个小朋友至少要分得一颗糖果。
- 分数高的小朋友要比旁边得分低的小朋友分得的糖果多。
你的任务是计算出最少需要分发多少颗糖果。
输入例子:
[1,2,2]
输出例子:
4
2、题目分析
这个问题可以分解为两个步骤:
- 从左到右遍历:确保每个小朋友的糖果数比左边评分低的小朋友多。
- 从右到左遍历:确保每个小朋友的糖果数比右边评分低的小朋友多。
通过这两个步骤,我们可以确保每个小朋友的糖果数既满足与左边小朋友的比较,也满足与右边小朋友的比较。
代码实现
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int candy(vector<int>& ratings) {
int num = ratings.size();
int result = 0;
// 这个初始化很重要
vector<int> sum(num, 1); // 创建一个大小为 num 的动态整型数组
if(num == 0){
return 0;
}else{
// 向左遍历,保证右边分高的糖果比左边的多
for(int i = 1; i < num; i++){
if (ratings[i-1] < ratings[i]) {
sum[i] = sum[i-1] + 1;
}
}
// 向右遍历,保证左边分高的糖果比右边的多,同时要保证右边分高的糖果比左边分低的多
for(int j = num-1; j > 0; j--){
if (ratings[j-1] > ratings[j]) {
// 这是全文的核心语句
sum[j-1] = max(sum[j-1], sum[j] + 1); // 如果不取最大值的话,有可能无法保证右边分高的糖果比左边分低的多
}
}
}
// 返回最少的糖果数
for(int i = 0; i < num; i++){
result += sum[i];
}
return result;
}
};
语法介绍
以下是代码中用到的关键语法和功能的表格形式说明:
语法/功能 | 描述 |
---|---|
vector<int> sum(num, 1); | 创建一个大小为 num 的 vector ,并将每个元素初始化为 1。 |
max(a, b) | 返回 a 和 b 中的最大值。 |
for 循环 | 用于遍历数组或容器中的元素。 |
详细说明
语法/功能 | 示例代码 | 作用 |
---|---|---|
vector<int> sum(num, 1); | vector<int> sum(5, 1); | 创建一个包含 5 个元素的 vector ,每个元素的值初始化为 1。 |
max(a, b) | int result = max(3, 5); | 返回 3 和 5 中的最大值,即 result = 5 。 |
for 循环 | for(int i = 0; i < num; i++) { cout << sum[i] << " "; } | 遍历 sum 数组,并打印每个元素的值。 |
3、其他思路
3.1 大佬思路1
通过动态规划和状态切换来解决分发糖果的问题。它使用了一个二维数组来存储两个状态(两行)的糖果分配情况,并通过不断更新这两个状态来确保每个小朋友的糖果数满足题目要求。来源:牛客网
1. 初始化
- 创建一个二维数组
v
,其中v[0]
和v[1]
分别表示两个状态(两行)的糖果分配情况。 - 初始化每个小朋友的糖果数为 0。
- 将第一行
v[0]
的每个小朋友的糖果数初始化为 1(因为每个小朋友至少分到一颗糖果)。
2. 状态切换与更新
- 使用两个标志变量:
bc
:用于判断是否需要继续更新糖果分配。如果某一轮遍历中没有更新任何糖果数,则退出循环。inturn
:用于切换当前使用的行(v[0]
或v[1]
)。
- 通过
pre
和pos
变量来标识当前使用的行和下一轮要更新的行。
3. 遍历与比较
- 从第二个小朋友开始遍历
ratings
数组,逐个处理每个小朋友的糖果分配。 - 对于每个小朋友
j
,首先将当前行的值复制为前一行的值(v[pos][j] = v[pre][j]
)。 - 然后分别与左邻居和右邻居进行比较:
与左邻居比较
- 如果当前小朋友的评分
ratings[j]
大于左邻居的评分ratings[j-1]
,则需要确保当前小朋友的糖果数比左邻居多。 - 如果当前小朋友的糖果数
v[pre][j]
小于或等于左邻居的糖果数v[pos][j-1]
,则将当前小朋友的糖果数更新为v[pos][j-1] + 1
。
与右邻居比较
- 如果当前小朋友的评分
ratings[j]
大于右邻居的评分ratings[j+1]
,则需要确保当前小朋友的糖果数比右邻居多。 - 如果当前小朋友的糖果数
v[pos][j]
小于或等于右邻居的糖果数v[pre][j+1]
,则将当前小朋友的糖果数更新为v[pre][j+1] + 1
。
4. 更新标志变量
- 如果在某次遍历中更新了某个小朋友的糖果数,则将
bc
设置为true
,表示需要继续循环。 - 如果某次遍历中没有更新任何糖果数,则退出循环。
5. 计算总糖果数
- 遍历结束后,根据
inturn
的值确定当前使用的行(v[0]
或v[1]
)。 - 累加当前行中所有小朋友的糖果数,得到最少需要分发的糖果总数。
6. 关键点
- 状态切换:通过
inturn
变量在v[0]
和v[1]
之间切换,避免直接修改当前行的数据。 - 动态更新:在遍历过程中动态调整每个小朋友的糖果数,确保评分高的小朋友分到的糖果更多。
- 退出条件:当某次遍历中没有更新任何糖果数时,退出循环,避免不必要的计算。
7. 复杂度分析
- 时间复杂度:O(N^2),其中 N 是小朋友的数量。最坏情况下,每次遍历都需要更新所有小朋友的糖果数。
- 空间复杂度:O(N),使用了一个二维数组
v
来存储两个状态的糖果分配情况。
这段代码通过动态规划和状态切换的方式,巧妙地解决了分发糖果的问题。它通过不断更新两个状态的糖果分配情况,确保每个小朋友的糖果数满足题目要求。虽然时间复杂度较高,但通过状态切换和动态更新的方式,代码实现更加灵活,适用于评分变化较为复杂的情况
。
int candy(vector<int> &ratings) {
// 创建一个二维向量 v,用于存储两个状态的糖果分配
vector<vector<int>> v(2, vector<int>());
for(int i = 0; i < ratings.size(); i++) {
// 初始化每个孩子的糖果数量为 0
v[0].push_back(0);
v[1].push_back(0);
}
// 初始化,每个孩子至少有一个糖果
for(int i = 0; i < ratings.size(); i++)
v[0][i] = 1;
bool bc = true; // 标志变量,用于指示是否需要继续循环
bool inturn = true; // 标志变量,用于指示当前使用哪一行
int pre, pos; // 用于存储前一行和当前行的索引
// 从第二个孩子开始,比较与前一个和后一个孩子的评分
for(int i = 1; i < ratings.size(); i++) {
if(!bc) break; // 如果没有需要更新的情况,退出循环
bc = false; // 重置标志变量
// 轮流切换两行
if(inturn) {
pre = 0; // 前一行索引
pos = 1; // 当前行索引
inturn = false; // 切换到下一轮
} else {
pre = 1; // 前一行索引
pos = 0; // 当前行索引
inturn = true; // 切换到下一轮
}
// 与左邻居和右邻居比较
for(int j = 0; j < ratings.size(); j++) {
v[pos][j] = v[pre][j]; // 先将当前行的值复制为前一行的值
// 比较与左邻居
if(j > 0) {
if(ratings[j] > ratings[j - 1]) {
// 如果当前孩子的评分高于左邻居,检查糖果数量
if(v[pre][j] <= v[pos][j - 1]) {
v[pos][j] = v[pos][j - 1] + 1; // 增加当前孩子的糖果数量
if(!bc) bc = true; // 如果有更新,设置标志变量
}
}
}
// 比较与右邻居
if(j < ratings.size() - 1) {
if(ratings[j] > ratings[j + 1]) {
// 如果当前孩子的评分高于右邻居,检查糖果数量
if(v[pos][j] <= v[pre][j + 1]) {
v[pos][j] = v[pre][j + 1] + 1; // 增加当前孩子的糖果数量
if(!bc) bc = true; // 如果有更新,设置标志变量
}
}
}
}
}
// 计算最小糖果总数
int minCan = 0;
if(inturn) pos = 0; // 如果当前是第一行
else pos = 1; // 如果当前是第二行
// 累加当前行的糖果数量
for(int i = 0; i < ratings.size(); i++)
minCan += v[pos][i];
return minCan; // 返回最小糖果数量
}
作者:天晴了吗
链接:https://www.nowcoder.com/exam/test/85856216/submission?pid=153
来源:牛客网
3.2 大佬思路2
代码思路介绍
通过一次遍历来解决分发糖果的问题,同时处理了评分相等的情况。相比于传统的两次遍历(从左到右和从右到左),这种方法在遍历过程中动态调整糖果分配,确保满足题目要求。来源:牛客网
1. 初始化
- 首先,代码检查输入数组
ratings
的长度n
,如果长度为 0,直接返回 0,因为没有小朋友需要分发糖果。 - 创建一个大小为
n
的数组res
,用于存储每个小朋友分到的糖果数。 - 初始化第一个小朋友的糖果数为 1(因为每个小朋友至少分到一颗糖果)。
2. 从左到右遍历
- 从第二个小朋友开始遍历
ratings
数组,逐个处理每个小朋友的糖果分配。 - 对于每个小朋友
i
,根据其评分与前一个小朋友i-1
的评分关系,分为以下三种情况:
情况 1:当前小朋友的评分 > 前一个小朋友的评分
- 如果当前小朋友的评分比前一个小朋友高,那么当前小朋友的糖果数应该比前一个小朋友多 1。
- 更新
res[i] = res[i-1] + 1
。
情况 2:当前小朋友的评分 == 前一个小朋友的评分
- 如果当前小朋友的评分与前一个小朋友相等,那么当前小朋友只需要分到 1 颗糖果。
- 更新
res[i] = 1
。
情况 3:当前小朋友的评分 < 前一个小朋友的评分
- 如果当前小朋友的评分比前一个小朋友低,那么当前小朋友只需要分到 1 颗糖果。
- 但是,如果前一个小朋友的糖果数也是 1,那么需要回溯调整前一个小朋友的糖果数,以确保评分高的小朋友分到的糖果更多。
- 从当前小朋友的前一个小朋友开始,向前遍历,逐个增加糖果数,直到不再满足递减条件(即
res[j] < res[j-1]
)。 - 在调整过程中,如果遇到评分相等的情况(
ratings[j] == ratings[j+1]
),则不需要继续调整。
- 从当前小朋友的前一个小朋友开始,向前遍历,逐个增加糖果数,直到不再满足递减条件(即
3. 计算总糖果数
- 遍历结束后,
res
数组中存储了每个小朋友分到的糖果数。 - 通过累加
res
数组中的所有值,得到最少需要分发的糖果总数。
4. 关键点
- 评分相等的情况:当两个相邻小朋友的评分相等时,只需要分给当前小朋友 1 颗糖果,不需要额外调整。
- 回溯调整:当当前小朋友的评分比前一个小朋友低时,如果前一个小朋友的糖果数也是 1,则需要向前回溯,逐个增加糖果数,以确保评分高的小朋友分到的糖果更多。
5. 复杂度分析
- 时间复杂度:O(N),其中 N 是小朋友的数量。虽然在某些情况下需要回溯调整,但总体时间复杂度仍然是线性的。
- 空间复杂度:O(N),需要额外的数组
res
来存储每个小朋友的糖果数。
class Solution {
public:
int candy(vector<int> &ratings) {
int n=ratings.size();
if(n==0) return 0;
vector<int> res(n);//存每个小孩应发的糖果数
res[0]=1;
for(int i=1;i<n;i++){//不断遍历ratings并更新res
//如果新加入的小孩大于前一个小孩,则糖果比前一个小孩+1
if(ratings[i]>ratings[i-1]) {
res[i]=res[i-1]+1;
}
//关键:如果等于前一个小孩,则新小孩只需要分一个糖果,res不需要更新
else if(ratings[i]==ratings[i-1]) {
res[i]=1;
}
//如果小于前一个小孩,则新加入小孩分一个糖果,之前的小孩应该比他多分到糖果
else{
res[i]=1;
//如果前面的小孩只有一个糖果,需要更新res
if(res[i-1]==1) {
//前面的小孩都多分一个糖果,直到递增结束
int j=i-1;
for(;j!=0&&res[j]<res[j-1];j--){
res[j]++;
}
//关键:注意相等rating的情况
if(res[j]<=res[j+1] && ratings[j]!=ratings[j+1])res[j]++;
}
}
}
int sum=0;
for(auto e:res){
sum+=e;
}
return sum;
}
};
作者:Joe2016
链接:https://www.nowcoder.com/exam/test/85856216/submission?pid=153
来源:牛客网
这段代码通过一次遍历和动态调整的方式,巧妙地解决了分发糖果的问题。它不仅处理了评分递增和递减的情况,还特别处理了评分相等的情况,确保每个小朋友分到的糖果数满足题目要求。相比于传统的两次遍历方法,这种方法在代码实现上更加简洁,同时保持了较高的效率。
拓展
1. 贪心算法
贪心算法是一种在每一步选择中都采取当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。在这个问题中,我们通过两次遍历来确保每个小朋友的糖果数满足条件,这就是贪心算法的应用。
2. 动态规划
虽然这个问题可以用贪心算法解决,但也可以考虑使用动态规划。动态规划通常用于解决具有重叠子问题和最优子结构性质的问题。在这个问题中,每个小朋友的糖果数依赖于其左右邻居的糖果数,因此可以考虑使用动态规划来解决。
3. 复杂度分析
- 时间复杂度:O(N),其中N是小朋友的数量。我们只需要遍历数组两次。
- 空间复杂度:O(N),我们需要一个额外的数组来存储每个小朋友的糖果数。
总结
通过这个问题,我们不仅学习了如何在实际问题中应用贪心算法,还加深了对数组操作和动态规划的理解。希望这篇博文能帮助你在解决类似问题时更加得心应手。题目虽然不难,也希望你能有所收获。如果你有任何问题或建议,欢迎在评论区留言讨论!
每日一题专栏将持续更新,涵盖各种算法和数据结构问题,帮助你在编程的道路上不断进步。如果你喜欢这篇文章,请点赞、分享并关注我的专栏,获取更多精彩内容!