【基础算法】贪心算法基础

系列综述:
💞目的:本系列是个人整理为了秋招算法的,整理期间苛求每个知识点,平衡理解简易度与深入程度。
🥰来源:材料主要源于代码随想录进行的,每个算法代码参考leetcode高赞回答和其他平台热门博客,其中也可能含有一些的个人思考。
🤭结语:如果有帮到你的地方,就点个赞关注一下呗,谢谢🎈🎄🌷!!!
🌈数据结构基础知识总结篇



😊点此到文末惊喜↩︎


一、贪心算法理论基础

定义

  1. 贪心策略的选择
    • 无后效性:当前做的选择不会影响未来做的选择,则该问题可通过所选择的贪心策略得到最优解
    • 性价比高:贪心算法求得的较优解总能够满足是最优解的一个 1 − 1 e 1 - \frac {1} {e} 1e1(63%) 近似
    • 工程实践:通过对于局部最优举反例的方式,如果不能达到全局最优,则换其他策略
  2. 一般步骤
    • 将问题分解为若干个子问题
    • 找出适合的贪心策略
    • 求解每一个子问题的最优解
    • 将局部最优解堆叠成全局最优解
  3. 贪心框架
    void greedyAlgorithm(container<type> con){
    	// 健壮性检查
    	// 基本初始化
    	// 贪心策略的迭代
    	// 返回结果
    	return ;
    }
    
  4. 贪心策略的选择
    • 图形化:将数据图形化,求特征
    • 路径法:顺序遍历

动态规划对状态空间的遍历构成一张有向无环图,遍历就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的「状态」,图中的边则对应状态之间的「转移」,转移的选取就是动态规划中的「决策」。


二、贪心算法基本题目

摆动序列

  1. 376. 摆动序列
    • 贪心策略:将摆动进行图形化,记录峰值个数
    • 本质:以返回值为目标,减少不必要的中间介质。
    • 思路:本题可以通过删除元素找到子序列长度,所以可以只求波峰和波谷而得到数量,但不用真删除元素
    • 尽量减少工作变量,如本题中,求峰值需要三个工作变量,但是只需要中间的两个差值。
    int wiggleMaxLength(vector<int>& nums) {
        // 健壮性检查
        if (nums.size() <= 1) 
            return nums.size();
        // 初始化
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
                preDiff = curDiff;
            }
        }
        return result;
    }
    

53. 最大子数组和

  1. 53. 最大子数组和
    • 贪心策略:路径法,顺序遍历路径元素,求路径上的最大和,如果出现负数则重开路径
    int wiggleMaxLength(vector<int>& nums) {
        // 健壮性检查
        if (nums.size() <= 1) 
            return nums.size();
        // 初始化
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
                preDiff = curDiff;
            }
        }
        return result;
    }
    

122. 买卖股票的最佳时机 II

  1. 122. 买卖股票的最佳时机 II
    • 图形化:向上的特征即为利润增加
    • 贪心策略
      • 空间开销小:利润>0就买入第二天卖出(贪心法应该单步考虑局部最优解)
      • 时间开销小:只要明天比今天股价高,就买入并持有。(单次买入,只要涨就不卖出)
    int maxProfit(vector<int>& prices) {
        int profit = 0;
        for(int i = 0; i + 1 < prices.size(); ++i) 
            profit += max(prices[i + 1] - prices[i], 0);
        return profit;
    }
    

55. 跳跃游戏

  1. 55. 跳跃游戏
    • 迭代范围也可以进行更改
    • 贪心策略
      • 每次在可选范围内,跳最大的
    bool canJump(vector<int>& nums) {
        int cover = 0;
        if (nums.size() == 1) return true; 
        // 每次更改迭代范围范围  
        for (int i = 0; i <= cover; i++) { 
            cover = max(i + nums[i], cover);
            if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
        }
        return false;
    }
    

45. 跳跃游戏 II

  1. 45. 跳跃游戏 II
    int jump(vector<int>& nums)	{
        int ans = 0;
        int end = 0;
        int maxPos = 0;
        for(int i = 0; i < nums.size() - 1; i++){
            maxPos = max(nums[i] + i, maxPos);
            if(i == end){
                end = maxPos;// 每次从结束位置开始,而不是起跳位置
                ans++;
            }
        }
        return ans;
    }
    
    

1005. K 次取反后最大化的数组和

  1. 1005. K 次取反后最大化的数组和
    • sort的使用:第三个参数为自定义的排序队则,在头文件#include
    • accumulate的使用:第三个参数为累加的初值,在头文件include
    static bool cmp(int a, int b) {
        return abs(a) > abs(b);// 绝对值的从大到小进行排序
    }
    int largestSumAfterKNegations(vector<int>& A, int K) {
    	// 将容器内的元素按照绝对值从大到小进行排序
        sort(A.begin(), A.end(), cmp); 
        // 在K>0的情况下,将负值按照绝对值从大到小依次取反
        for (int i = 0; i < A.size(); i++) { 
            if (A[i] < 0 && K > 0) {
                A[i] *= -1;
                K--;
            }
        }
        // 如果K为奇数,将最小的正数取反
        if (K % 2 == 1) 
        	A[A.size() - 1] *= -1; 
       	// 求和
        return accumulate(A.begin(),A.end(),0);
        // 第三个参数为累加的初值,在头文件include<numeric>
    }
    

135. 分发糖果

  1. 135. 分发糖果
    • 双向遍历进行贪心处理
    int candy(vector<int>& ratings) {
        vector<int> candyVec(ratings.size(), 1);
        // 从前向后
        for (int i = 1; i < ratings.size(); i++) {
            if (ratings[i] > ratings[i - 1]) 
                candyVec[i] = candyVec[i - 1] + 1;
        }
        // 从后向前
        for (int i = ratings.size() - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1] ) {
                candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
            }
        }
        // 统计结果
        int result = 0;
        for (int i = 0; i < candyVec.size(); i++) result += candyVec[i];
        return result;
    }
    

406. 根据身高重建队列

  1. 406. 根据身高重建队列
    • 两个维度遍历进行贪心处理
    • 常用插入操作使用list进行处理
    • 感觉可以局部最优推出整体最优,而且想不到反例。就可以使用贪心算法。
    // 身高从大到小排(身高相同k小的站前面)
    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) {
        sort (people.begin(), people.end(), cmp);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        
        for (int i = 0; i < people.size(); i++) {// 按序插入
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
    

452. 用最少数量的箭引爆气球

  1. 452. 用最少数量的箭引爆气球
    • 贪心算法通常先进行排序最值处理
    // 基本比较函数的使用:1.const &形参 2. 形参为比较的两个数据元素 3. 返回值为两个形参的比较
    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];
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        if(points.size() == 0)
            return 0;
        int count = 1;// 至少射一支箭
        sort(points.begin(), points.end(), cmp);
        for(int i = 1; i < points.size(); ++i){
            if(points[i-1][1] < points[i][0]){
                count++;             
            }else{// 记录最小下限
                points[i][1] = min(points[i - 1][1], points[i][1]);
            }
        }
        return count;
    }
    

763. 划分字母区间

  1. 763. 划分字母区间
    • 如果只有字母,可以使用数组进行哈希映射。 vector<int> alphabet = {27,0}
    vector<int> partitionLabels(string s) {
        // 统计每个字符的最远坐标
        unordered_map<char, int> umap;
        for(int i = 0; i < s.size(); ++i){
            umap[s[i]] = i;
        }
    	// 初始化
        vector<int> res;
        int left = 0;
        int right = 0;
        // 迭代
        for(int i = 0; i < s.size(); ++i){
            right = max(right, umap[s[i]]); // 找到字符出现的最远边界
            if (i == right) {
                res.push_back(right - left + 1);// 结果记录
                left = i + 1;
            }
        }
        return res;
        
    }
    

435. 无重叠区间

  1. 435. 无重叠区间
    • 能读就不写:求数量的尽量不要改变改变原来数组,减少写操作。
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 1; // 记录非交叉区间的个数
        int end = intervals[0][1]; // 记录区间分割点
        for (int i = 1; i < intervals.size(); i++) {
            if (end <= intervals[i][0]) {// 记录最小的右边界
                end = intervals[i][1];
                count++;
            }
        }
        return intervals.size() - count;
    }
    

56. 合并区间

  1. 56. 合并区间
    • 排序参数lambda表达式的使用
    • 遍历的容器尽量不用动,使用新的结果容器进行处理
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
    	// 健壮性检查
    	if (intervals.size() == 0) 
    		return result; // 区间集合为空直接返回
        vector<vector<int>> result;
        // 排序的参数使用了lambda表达式
        sort(intervals.begin(), intervals.end(), 
        	[](const vector<int>& a, const vector<int>& b){
        		return a[0] < b[0];
        	}
        );
        // 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并
        result.push_back(intervals[0]); 
    	// 合并区间,只更新右边界就好,
    	// 因为result.back()的左边界一定是最小值,因为我们按照左边界排序的
        for (int i = 1; i < intervals.size(); i++) {
            if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间
                result.back()[1] = max(result.back()[1], intervals[i][1]); 
            } else {
                result.push_back(intervals[i]); // 区间不重叠 
            }
        }
        return result;
    }
    

738. 单调递增的数字

  1. 738. 单调递增的数字
    • 数字转换成字符串处理
    int monotoneIncreasingDigits(int N) {
        string strNum = to_string(N);
        // flag用来标记赋值9从哪里开始
        // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
        int flag = strNum.size();
        for (int i = strNum.size() - 1; i > 0; i--) {
            if (strNum[i - 1] > strNum[i] ) {
                flag = i;
                strNum[i - 1]--;
            }
        }
        for (int i = flag; i < strNum.size(); i++) {
            strNum[i] = '9';
        }
        return stoi(strNum);
    }
    

少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
不如点赞·收藏·关注一波


🚩点此跳转到首行↩︎

参考博客

  1. 代码随想录
  2. letcode
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆羽飘扬

如果有用,请支持一下。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值