leetcode贪心策略篇总结(C++)

文章目录

  • 一、基础知识
    • 1、什么是贪心策略?
    • 2、什么时候可以用贪心?
    • 3、一般步骤
  • 二、代表题目
    • 455-分发饼干-简单
    • 860-柠檬水找零-简单
    • 摆动序列
      • 376-摆动序列-中等
      • 122-买卖股票的最佳时机II-中等
    • 最大子序列之和
      • 53-最大子序号-中等
      • 134-汽油站-中等
    • 跳跃区间
      • 55-跳跃游戏-中等
      • 45- 跳跃游戏II -中等
    • 两个维度的权衡
      • 135-分发糖果-困难
      • 406-通过身高重建队列-中等
    • 1005-k次取反求和-简单
    • 重叠区间相关题目
      • 452-用最少数量的箭引爆气球-中等
      • 435- 无重叠区间-中等
      • 763-划分字母区间-中等
    • 738-单调递增的数字-中等
    • 714-买卖股票的最佳时期含手续费
    • 968-监控二叉树-困难
  • 三、总结

注:按照代码随想录的刷题指南进行,自己总结补充,以加深印象
参考链接:https://leetcode-cn.com/circle/article/wGp7Y9/
题目来源:力扣(LeetCode)

一、基础知识

1、什么是贪心策略?

本质上是通过选取每一阶段的局部最优,最终达成全局最优

2、什么时候可以用贪心?

没有一般套路,大体可以先模拟一下,或者取反例证明不能用贪心。可以用数学证明贪心策略的正确性(数学归纳法/反证法),但显然难度过大。

3、一般步骤

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

难点在于如何通过局部最优得到全局最优

二、代表题目

455-分发饼干-简单

  • 饼干尺寸s[i], 小孩的胃口g[i], 给定数组s和g, 问得到满足的小孩(s[i] >= g[i])的最大人数题目链接
  • 这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
  • 一个错误点,使用sort的cmp函数时,显示错误reference to non-static member function must be called,错误解决参考下面的链接 错误参考

860-柠檬水找零-简单

  • 每人只能买一份5元的柠檬水,面值金额有5,10,20,bill数组列出每一个顾客拿出来的纸币金额,摊主最初没有钱,问能否给所有顾客都找好零钱题目链接
  • 贪心,和发小饼干差不多,我需要尽量把收到的高金额的钱找出去,也就是找钱时先用10块的,再用5块的
  • 找钱无非就10块和20块的情况,细分下来并不复杂
  • 刚开始直接初始化了一个大小21的数组做hash表示摊主拥有的5,10,20的数量,但其实节省空间,维护三个变量 five、ten、twenty 表示当前手中拥有的 55 美元和 10、20 钞票的张数即可

摆动序列

376-摆动序列-中等

  • 求一个数组的摆动序列的最长长度(可以不连续)题目链接
  • 贪心解法
    局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
    整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
    关键点是把问题转化为求有几个局部峰值
    算法的细节注意,数组的最左侧和最右侧比较复杂,实现起来不简单,看程序
int wiggleMaxLength(vector& 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;
    }
  • 动态规划
  1. dp[i]是0-i的数组的摆动序列最长长度,但nums[i]在摆动序列里有两个状态,一个是作为山峰,一个是作为山谷
    因此动态规划矩阵为: dp[i][0] 代表山峰, dp[i][1] 代表山谷
  2. 状态转移矩阵
    dp[i][0] = max{dp[i][0], dp[j][1] + 1}, j < i, 且nums[i] > nums[j]
    dp[i][1] = max{dp[i][1], dp[j][0] + 1}, j < i, 且nums[i] < nums[j]
  3. 初始化:
    全部为1 !!!! 可以理解为,自身作为摆动序列的起点(因为状态转移中没有考虑它自身作为起点的情况);

122-买卖股票的最佳时机II-中等

  • 可以买卖多次,问赚到的最大金额题目链接
  • 动态规划解法,在动态规划专题,略
  • 贪心解法: 可以看作一个起伏的曲线(摆动序列题的变式),那么在局部最低点买,局部最高点卖就可以达到全局最优
  • 注意最高点要判断是否有股票

最大子序列之和

53-最大子序号-中等

  • 求数组的连续子串的最大和题目链接
  • 动规解法,在动规专题,略
  • 贪心解法

如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。

134-汽油站-中等

  • 一个由汽油站构成的圆,gas数组列出每一个汽油站可加汽油数,cost数组列出从汽油站i到汽油站i+1所花费的汽油数,问汽车是否可以成功转一圈,以及成功时需要从哪个汽油站开始题目链接
  • 一开始没有思路,后来回顾了一下之前做过的题,发现和最长子序列问题异曲同工
  • 贪心,把两个数组相减变成一个差分数组,我们要保证路程前期尽可能多得到汽油,所以就变成了最长子序列问题,但注意这是一个圆,求出发汽油站的索引要注意取模

跳跃区间

55-跳跃游戏-中等

  • nums[i]表示在位置i可以跳跃的最大步数,从index= 0开始,问是否可以跳跃到最后一个位置题目链接
  • 注意点:在位置i的跳跃步数可以<= nums[i]
  • 动态规划解法
    dp[i]代表是否可以从index= 0到达index = i
    到达index= i, 则判断所有j ,j < i &&dp[j] == true , if(j + nums[j] >= i ) ,那么dp[i] = true
    dp[0] = true;
  • 贪心解法
    这道题目关键点在于:不用拘泥于每次究竟跳跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
    那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
    贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
    实现上就在cover覆盖的范围内,浏览每一个位置,让覆盖范围扩大

45- 跳跃游戏II -中等

  • 一定可以到达,求最小跳跃数题目链接
  • 贪心策略:以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点
    实现:在当前位置可以向外扩展的范围内,选择一个可以向外扩展最大的位置
  • 自主实现,还没看其他题解,待优化

两个维度的权衡

135-分发糖果-困难

  • 给小朋友分糖果,每人最少一个,且评分高于邻居的小朋友糖果也要多于邻居题目链接
  • 自己想半天没想出来,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。
  • 关键点:两次贪心的策略:
    一次是从左到右遍历,只比较右边孩子评分比左边大的情况,ans[i] = ans[i - 1] + 1;
    一次是从右到左遍历,只比较左边孩子评分比右边大的情况, ans[i] = max(ans[i], ans[i + 1] + 1);
    这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
  • 为什么第二次遍历不从前向后呢?因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果了。

406-通过身高重建队列-中等

  • 根据人们的身高和rank重建队列,其中rank是指该人在队列中前面有rank人的身高大于等于他题目链接

  • 这个关键点:确定一个维度(身高),再按另外一个维度(rank)排序 ,上题也是,先确定左侧的,再确定右侧的

  • 局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

  • 全局最优:最后都做完插入操作,整个队列满足题目队列属性

      static bool cmp(vector &a , vector &b){
          if(a[0] > b[0] || (a[0] == b[0] && a[1] < b[1]))
              return true;
          else
              return false;
      }
      vector> reconstructQueue(vector>& people) {
           sort(people.begin(), people.end(), cmp);
           vector> ans;
           ans.push_back(people[0]);
           for(int i = 1; i < people.size(); i++){
               int rank = people[i][1];
               //因为现在ans的元素一定是大于等于现在值的,所以直接按rank插入
               ans.insert(ans.begin() + rank, people[i]);
           }
          return ans;
      }
    
  • 但使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。所以使用vector(动态数组)来insert,是费时的.

  • 改为list结构(底层用链表实现,便于插入删除), list结构无法随机访问

  • vector用时5.27%, 内存57%;

  • list用时86%, 内存31%

1005-k次取反求和-简单

  • 对于一个数组,指定取反次数k, 可对一个数反复取反,求最后最大的数组和题目链接
  • 简单粗暴解法:最初是把数组从小到大排序,一次遍历,边遍历边取反。如果是负数,则转化,如果是0,转k次,如果是正数,转剩余次数,但接着就要注意取反一个正数和保留一个负数(比如-1, 9)的情况,虽然做出来了,但边界条件太多了,提交了好几次才把情况补全
  • 细节(来自官方答案)

然而注意到本题中数组元素的范围为 [-100, 100],因此我们可以使用计数数组(桶)或者哈希表,直接统计每个元素出现的次数,再升序遍历元素的范围,这样就省去了排序需要的时间。

  • 题解精选的写法(运行时间100%)链接

1.K>0,则执行 2,否则执行 4

2.取数组 A 中的最小值,并取反(用上面说的hash找最小值)

3.K-- 执行 1

4.对数组 A 求和

  • 贪心解法: 关键:按绝对值排序
    第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
    第二步:从前向后遍历,遇到负数将其变为正数,同时K–
    第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
    第四步:求和

重叠区间相关题目

通常需要排序,确定一个维度;
注意重叠的条件

452-用最少数量的箭引爆气球-中等

  • 一个气球的位置为[xstart , xend], 如果箭头的位置x 满足xstart <= x <= xend,则气球会引爆。给定一组气球的位置数组point, 问最少需要几根箭头引爆气球题目链接
  • 思想:显然要将重叠的气球归并成一个区间,最后看一共有几个这样的区间。归并的关键是先排序,确定一个维度
  • 注意点是[0,1][1,2]也算是相交
  • 做法:
    • 按start排序,确定一个维度
    • 求交集,如果当前气球和后面没有交集,则用一个箭头
  • 实现细节上,之前是用temp数组存储新的交集区间,简化版本是不用temp存储前一个不重叠的气球,而是直接修改points, 将属于该区间的气球的end直接修改。
  • 56-合并区间-中等一样的套路,只不过是返回不重叠的区间数组 question

435- 无重叠区间-中等

  • 对于数组intervals, intervals[i] = [starti, endi]; 问最小移除多少区间,让剩下的区间可以不重叠 question
  • 最开始的思路,排序后,如果遇到重复,那么就删除区间最大的元素。明确来说,是按照左边界排序后,我们要尽量选择区间最小(占地面积最小)的数值,保证尽可能多的添加元素
  • 另一种是看的代码随想录,本题其实和452.用最少数量的箭引爆气球非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。
for(int i = 1; i < intervals.size(); i++){
      //把弓箭那道题目代码里<= 换成<
      if(intervals[i][0] < intervals[i - 1][1]){
          //有交集
          intervals[i][1] = min(intervals[i-1][1], intervals[i][1]);
      }else{
          //无交集
          count++;
      }
  }

763-划分字母区间-中等

  • 划分字母串s, 确保每一个字母最多出现在一个part中。question
  • 方法1, 遍历s,得到每一个字母的【start,end】区间,问题变为求最小数目的不重叠区间(变成452题目)
  • 方法2,更简洁, 在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
    • 统计每一个字符最后出现的位置
    • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

738-单调递增的数字-中等

  • 如果一个数的各个位上的数字从左向右递增,则称为单调递增。给定一个n, 返回小于等于n的最大的单调递增数字question
  • 举个例子,98,最大的递增数字是89, 即个位9减去一, 十位上的8变成9,试图推演出解题思路
  • 局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]–,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。
    全局最优:得到小于等于N的最大单调递增的整数。
  • 注意点:遍历顺序要从后向前,因为从前向后的话,调整strNum[i-1]的值,可能会让其小于strNump[i-2]

714-买卖股票的最佳时期含手续费

  • 如题,question
  • 动态规划算法,见动态规划专题
  • 贪心算法,相当于按天算钱, 将手续费放在买入时进行计算, 对于这一天的价格:
    • prices[i] + fee < buy, 则更新最低价格
    • 获利是一天天累加的(当我们卖出一支股票时,我们就立即获得了以相同价格并且免除手续费买入一支股票的权利),只要当天的价格大于手续费+买入价格,那么就可以获利,
profit += prices[i] - buy;
buy = prices[i];

如果当天价格小于之前买入花的钱,就按兵不动,

968-监控二叉树-困难

  • 一个节点上的摄像头可以同时监控他本身,它的孩子,他的父母; 问最少几个摄像头可以监控整个二叉树 [question](https://leetcode.cn/problems/binary-tree-cameras/)
  • 还是一个确定顺序的过程;因为摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。所以我们要从下往上看
  • 局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少
  • 大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点
  • 做法:
    • 二叉树后序遍历,以方便回溯时从下向上;
    • 要做到隔两个节点放一个摄像头,需要判断每个节点的状态,进行状态推导
      一个节点有三种状态
      0:该节点无覆盖
      1:本节点有摄像头
      2:本节点有覆盖
	 int result = 0;
    //返回root节点的状态
    int traveral(TreeNode * root){
        if(root == nullptr)
        {
            return 2; //空节点视为状态2
        }
        //记录左右节点的状态,以判断父节点的状态
        int left = traveral(root->left);
        int right = traveral(root->right);
        
        if(left == 2 && right == 2){
            return 0; 
        }else if(left == 0 || right == 0){
            result++;
            return 1;
        }else 
            return 2;

    }

三、总结

1、贪心没有什么固定套路
2、注意到本题中数组元素的范围为 [-100, 100],因此我们可以使用计数数组(桶)或者哈希表,直接统计每个元素出现的次数,再升序遍历元素的范围,这样就省去了排序需要的时间。
3、很多题本质上是一样的,重点是如何把题目转化为可以解决的问题。
4、数据结构的选择在运行时间上也很重要,比如 通过身高重建队列的

你可能感兴趣的:(leetcode,leetcode,c++,算法)