贪心算法理解
这学习以下5 道题(柠檬水找零、分发糖果等)是贪心算法在生活场景、数值调整与队列重构中的典型应用,核心不依赖区间操作,而是 “策略设计 + 有序处理” 的结合。先针对问题提炼局部最优规则(如找零优先保 5 元、分糖果分两轮遍历),再通过排序(如按绝对值 / 身高排序)或定向遍历(左右遍历、从后往前),让每步局部最优自然累积为全局最优。学习《代码随想录》笔记。
1. 题目:135. 分发糖果(最少糖果数)
题目分析
n 个孩子站成一排,每个孩子至少 1 颗糖果,且相邻孩子中评分高的得更多糖果,求最少需准备的糖果数。核心是 “分两次遍历:先保证左到右的规则,再保证右到左的规则”。
代码解析
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
if(n == 0) return 0;
vector<int> candies(n, 1); // 初始化:每个孩子至少1颗糖果
// 第一次遍历:左到右,确保“右边评分高的孩子比左边多”
for(int i=0;i<n-1;i++){
if(ratings[i+1] > ratings[i]){
candies[i+1] = candies[i] + 1;
}
}
// 第二次遍历:右到左,确保“左边评分高的孩子比右边多”(不破坏左到右的结果)
for(int i=n-2;i>=0;i--){
if(ratings[i] > ratings[i+1]){
// 取max:保证既满足左>右,又不覆盖左到右时的更大值
candies[i] = max(candies[i], candies[i+1] + 1);
}
}
// 计算总糖果数
int total = 0;
for(int c : candies) total += c;
return total;
}
};
贪心思路提炼
- 局部最优 1(左到右):只关注右边比左边评分高的情况,给右边多 1 颗糖果。
- 局部最优 2(右到左):只关注左边比右边评分高的情况,给左边 “当前糖果数” 与 “右边 + 1” 的最大值。
- 全局最优:两次遍历后,所有相邻孩子的规则都满足,且总糖果数最少。
2. 题目:860. 柠檬水找零
题目分析
柠檬水售价 5 美元,顾客付 5/10/20 美元,初始无零钱,需正确找零(净收入 5 美元),判断能否给所有顾客找零。核心是 “优先用大面额零钱找零,减少小面额零钱消耗”。
代码解析
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
bool canChange = true;
int five = 0, ten = 0; // 记录5美元、10美元的数量(20美元无需记录,无法找零)
for(int bill : bills){
if(bill == 5){
five++; // 收5美元,无需找零,5元数量+1
}else if(bill == 10){
ten++; // 收10美元,需找5美元
if(five >= 1) five--;
else{
canChange = false;
break;
}
}else if(bill == 20){
// 收20美元,优先用10+5找零(减少10元消耗,保留更多5元找零)
if(ten >= 1 && five >= 1){
ten--;
five--;
}else if(five >= 3){ // 无10元时,用3张5元找零
five -= 3;
}else{
canChange = false;
break;
}
}
}
return canChange;
}
};
贪心思路提炼
- 局部最优:面对 20 美元找零时,优先用 10+5 的组合(而非 3 张 5 元),因为 5 元是找零 10 美元的唯一货币,需尽量保留。
- 全局最优:所有顾客都能正确找零。
3. 题目:1005. K 次取反后最大化的数组和
题目分析
给定整数数组和 K,每次可将任意元素取反,恰好执行 K 次,求修改后数组的最大可能和。核心是 “优先反转绝对值大的负数,最后若 K 剩奇数则反转最小绝对值的数(相当于反复反转)”。
代码解析
class Solution {
public:
// 自定义排序:按绝对值从大到小排,优先处理绝对值大的数
static bool cmp(const int &a, const int &b){
return abs(a) > abs(b);
}
int largestSumAfterKNegations(vector<int>& nums, int k) {
int sum = 0;
sort(nums.begin(), nums.end(), cmp); // 按绝对值降序排序
for(int i=0;i<nums.size()&&k>0;i++){
if(nums[i] < 0){ // 优先反转负数(绝对值大的先转)
nums[i] = -nums[i];
k--;
}
}
// 若K仍为奇数,反转最小绝对值的数(此时数组全非负,最后一个数绝对值最小)
if(k % 2 == 1) nums[nums.size()-1] *= -1;
// 计算最终数组和
for(int num : nums) sum += num;
return sum;
}
};
贪心思路提炼
- 局部最优 1:每次反转时,优先选择 “绝对值最大的负数”,让数组和增加最多。
- 局部最优 2:若 K 剩奇数,反转 “绝对值最小的数”,让数组和减少最少。
- 全局最优:经过 K 次反转后,数组和最大。
4. 题目:406. 根据身高重建队列
题目分析
给定 people 数组(每个元素 [hi, ki] 表示身高 hi,前面有 ki 个≥hi 的人),重建符合要求的队列。核心是 “先排身高高的人,再按 ki 插入对应位置”—— 身高高的人不受身高矮的人影响。
代码解析
class Solution {
public:
// 排序依据:按身高降序排,身高相同则按ki升序排
static bool cmp(const vector<int>& a, const vector<int>& b){
if(a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
vector<vector<int>> res;
sort(people.begin(), people.end(), cmp); // 按身高降序排序
// 按ki插入对应位置:ki即为该人在当前队列中的索引
for(int i=0;i<people.size();i++){
res.insert(res.begin() + people[i][1], people[i]);
}
return res;
}
};
贪心思路提炼
- 局部最优:先处理身高高的人,他们的 ki 直接对应队列中的位置(因为后续插入的人身高更矮,不影响他们的 ki 计数)。
- 全局最优:最终队列满足每个人的 [hi, ki] 要求。
5. 题目:738. 单调递增的数字
题目分析
给定整数 n,返回 “小于或等于 n 的最大单调递增数字”(单调递增指相邻位数 x≤y)。核心是 “从后往前找第一个递减位,将高位减 1,低位全设为 9”。
代码解析
class Solution {
public:
int monotoneIncreasingDigits(int n) {
if(n < 10) return n; // 个位数本身单调递增
vector<int> digits; // 存储n的每一位(从低位到高位)
while(n != 0){
digits.push_back(n % 10);
n /= 10;
}
int flag = 0; // 标记从哪一位开始需要设为9
// 从低位往高位遍历(digits[0]是个位,digits[1]是十位)
for(int i=0;i<digits.size()-1;i++){
if(digits[i] < digits[i+1]){ // 发现当前位(低位)小于高位,需调整
digits[i+1]--; // 高位减1
flag = i+1; // 标记:从当前高位的下一位(i)开始,所有低位设为9
}
}
// 将flag之前的低位(包括flag-1)全设为9
for(int i=0;i<flag;i++) digits[i] = 9;
// 重构数字
int res = 0;
for(int i=0;i<digits.size();i++){
res += digits[i] * pow(10, i);
}
return res;
}
};
贪心思路提炼
- 局部最优:找到第一个 “低位 < 高位” 的位置,将高位减 1(避免递减),并标记后续低位全设为 9(最大化数字)。
- 全局最优:最终得到的数字是小于等于 n 的最大单调递增数。
贪心算法总结
综合,跟随代码随想录,结合前面所学习的内容,学习了贪心算法的经典例题16道:
(一)场景 1:分配
场景特点:贴合现实规则(找零、分资源),核心是 “优先保留稀缺资源” 或 “最小资源满足最大需求”。
| 例题 | 核心问题 | 解题模板(局部最优→全局最优) | 代码关键要点 |
|---|---|---|---|
| 455. 分发饼干 | 用最少饼干满足最多孩子 | 1. 排序(胃口、饼干均升序);2. 双指针匹配(小饼干喂小胃口,避免浪费);3. 统计满足数。 | 时间复杂度O(nlogn+mlogn),双指针不越界,避免嵌套循环。 |
| 860. 柠檬水找零 | 能否正确找零(5/10/20 美元) | 1. 记录稀缺资源(5 美元,唯一能找 10 美元的货币);2. 大面额优先找零(20 美元先 10+5);3. 无资源则返回 false。 | 仅需记录 5/10 美元数量,20 美元无需存储,减少变量冗余。 |
| 135. 分发糖果 | 最少糖果满足 “相邻高评分多拿” | 1. 初始化(每人 1 颗);2. 两次遍历(左→右保右高左多,右→左保左高右多,取 max 避冲突);3. 累加总糖果。 | 两次遍历避免 “单向判断遗漏”,max操作不破坏前序结果。 |
(二)场景 2:序列优化
场景特点:围绕 “数组 / 序列的差值、和、利润” 优化,核心是 “忽略无效局部(平坡、负数),聚焦有效变化”。
| 例题 | 核心问题 | 解题模板(局部最优→全局最优) | 代码关键要点 |
|---|---|---|---|
| 376. 摆动序列 | 最长交替差值子序列 | 1. 定义prediff(前差值)、curdiff(当前差值);2. 检测摆动(升转降 / 降转升)时计数 + 1;3. 平坡不更新prediff。 | 初始prediff=0适配序列开头,避免平坡误判,时间复杂度O(n)。 |
| 53. 最大子数组和 | 连续子数组的最大和 | 1. 维护count(当前子数组和)、result(最大和);2. 累加count并更新result;3. count≤0时重置(负数拉低后续和)。 | 初始result=INT_MIN适配全负数,重置时机是 “当前和≤0”,而非 “单个元素负”。 |
| 122. 买卖股票 II | 多次交易的最大利润 | 1. 遍历序列,计算 “后一天 - 前一天” 的差价;2. 仅累加正差价(赚每段上涨利润);3. 下跌差价忽略。 | 本质是 “拆分所有上涨波段”,时间复杂度O(n),无需记录买卖时机。 |
| 134. 加油站 | 环形路线的有效起点 | 1. 计算总净油量(gas[i]-cost[i]),总和 < 0 则无解;2. 维护currentSum(当前累计净油);3. currentSum<0时重置起点为下一站。 | 核心证明:总净油≥0 必存在解,重置起点时无需回退,一次遍历完成。 |
(三)场景 3:覆盖
场景特点:围绕 “范围覆盖”(数组跳跃、区间重叠、字符串片段),核心是 “最大化局部覆盖范围,减少操作次数”。
| 例题 | 核心问题 | 解题模板(局部最优→全局最优) | 代码关键要点 |
|---|---|---|---|
| 55. 跳跃游戏 | 能否到达数组终点 | 1. 维护maxCover(当前最大覆盖下标);2. 遍历覆盖范围内的位置,更新maxCover;3. 覆盖终点则返回 true。 | 遍历范围严格限制在maxCover内,避免无效位置,提前终止提升效率。 |
| 45. 跳跃游戏 II | 到达终点的最少跳跃次数 | 1. 定义curCover(当前覆盖终点)、nextCover(下一跳最大覆盖);2. 达curCover时跳跃 + 1,更新curCover=nextCover;3. 覆盖终点则提前退出。 | 不记录具体路径,仅关注覆盖边界,确保每跳效率最高。 |
| 452. 箭引爆气球 | 最少箭数引爆所有气球 | 1. 按左边界升序排序(左同则右升);2. 重叠时更新下一个气球右边界为 “当前最小右边界”;3. 无重叠则箭数 + 1。 | 排序是前提,通过 “缩小右边界” 最大化单箭覆盖,时间复杂度O(nlogn)。 |
| 435. 无重叠区间 | 最少移除区间数 | 1. 按左边界升序排序;2. 重叠时移除右边界大的区间(更新为最小右边界),计数 + 1;3. 无重叠则继续。 | 本质是 “保留最多不重叠区间”,移除右边界大的区间减少后续重叠。 |
| 763. 划分字母区间 | 最多片段且字母不跨片段 | 1. 哈希表存每个字母的最远出现位置;2. 遍历更新片段右边界(取当前字母最远位置);3. 达右边界则划分片段。 | 哈希表提前初始化最远位置,避免重复遍历,片段划分时记录长度而非具体字符。 |
(四)场景 4:数值排序与队列重构
场景特点:需打破无序状态,核心是 “按关键维度排序(绝对值、身高),再定向调整数值或插入位置”。
| 例题 | 核心问题 | 解题模板(局部最优→全局最优) | 代码关键要点 |
|---|---|---|---|
| 1005. K 次取反 | 取反 K 次后的最大数组和 | 1. 按绝对值降序排序(优先处理影响大的数);2. 反转负数(K 递减);3. K 剩奇数时反转最小绝对值数。 | 排序维度是 “绝对值” 而非 “数值大小”,避免小负数优先反转导致和减少。 |
| 406. 根据身高重建队列 | 按 [hi, ki] 重构队列 | 1. 按身高降序、ki 升序排序(高个子不受矮个子影响);2. 遍历按 ki 插入队列(ki 即当前索引);3. 返回最终队列。 | 排序后插入无需调整已有元素,insert操作虽为O(n²),但思路直观且适配问题。 |
| 738. 单调递增的数字 | 小于等于 n 的最大单调数 | 1. 拆数字为 “低位到高位” 数组;2. 从后往前找递减位,高位减 1 并标记低位;3. 标记位前低位全设 9。 | 从后往前遍历避免 “连锁递减”(如 332→329→299),重构数字时注意位权。 |
579

被折叠的 条评论
为什么被折叠?



