贪心算法学习(03)(代码随想录)

贪心算法理解

这学习以下5 道题(柠檬水找零、分发糖果等)是贪心算法在生活场景、数值调整与队列重构中的典型应用,核心不依赖区间操作,而是 “策略设计 + 有序处理” 的结合。先针对问题提炼局部最优规则(如找零优先保 5 元、分糖果分两轮遍历),再通过排序(如按绝对值 / 身高排序)或定向遍历(左右遍历、从后往前),让每步局部最优自然累积为全局最优。学习《代码随想录》笔记。

1. 题目:135. 分发糖果(最少糖果数)

题目分析

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. 柠檬水找零

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 次取反后最大化的数组和

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. 根据身高重建队列

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. 单调递增的数字

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),重构数字时注意位权。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值