[算法数据结构] 四天总结贪心算法主要题型

前言

第一天

贪心算法,没有固定的套路,基本的思维就是局部最优推出全局最优。难点在于如何将问题分解成子问题。下面总结一下三道题:分发饼干,摆动序列,最大子序和。回想一下没有固定的框架。

  • 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

问题解决的关键逻辑:让大的饼干尽可能满足胃口大的。

  • 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。 例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

问题的关键:通过累加每一个山峰和山底来找到最长摆动子序列的数量。

  • 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

问题的关键:当累加数组的为负数时,就重新从0开始累加。

第二天

昨天共做以下几道题目:股票问题、跳跃游戏、跳跃游戏2、加油站、K次取反、分发糖果、柠檬水找零、根据身高重建队列。其中独立思考做出来的时股票问题、K次取反、柠檬水找零。 有点儿难度的题目:跳跃游戏、跳跃游戏2、加油站。前两个属于区间类型的题目。 中等题目:两个维度权衡的问题:分发糖果、根据身高重建队列。

  1. 区间问题
  • 跳跃游戏

一开始想的是每一步都跳最远,在遇到0的情况下,如何解决,思考的是回退。关键思路是:记录最大的cover的值,然后再在0-cover区间内进行遍历,找到再大的cover值。

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。 假如为[2, 5, 0, 0]

  • 跳跃游戏2

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。

要统计次数,就需要考虑什么时候必须加一次。

class Solution {
public:
    int jump(vector& nums) {
        if (nums.size() == 1) return 0;
        int curDistance = 0;
        int ans = 0;
        int nextDistance = 0;
        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nums[i] + i, nextDistance);
            if (i == curDistance) {
                if (curDistance != nums.size() - 1) {
                    ans++;
                    curDistance = nextDistance;
                } else break;
            }
        } 
        return ans;
    }
};
复制代码

依次遍历数组的数据,若走到当前最大距离(curDistance)还没有走到终点,那么就必须走下一步,把在当前这段(curDistance)中记录的下一步nextDistance更新为curDistance。并且步数加1。

  • 加油站

    加油站一开始的思路是模拟每一站的消耗与补充,没有整体的思考其中的规律。首先是记录每一次经过加油站的rest,并且将其累加,如果累加起来是负数,那么绝无可能绕一圈,消耗的油比补充的油少。并且在这个过程可以维护累积透支的油cur_sum。如果cur_sum是>=0的,那么说明,每一次都刚好能到下一个加油站,并且接受补给。开始的地点为0。 假如cur_sum是负数呢?从最后一个加油站往前看是否能有攒下来的油弥补cur_sum,即透支的油。假如能将其弥补,意味着能够从这个加油站出发,攒油,并且供后面的加油站消耗。返回这个位置i。

          class Solution {
      public:
          int canCompleteCircuit(vector& gas, vector& cost) {
              int curSum = 0;
              int min = INT_MAX;
              for (int i = 0; i < gas.size(); i++) {
                  int rest = gas[i] - cost[i];
                  curSum += rest;
                  if (curSum < min) {
                      min = curSum;
                  }
              }
    
              if (curSum < 0) return -1;
              if (min >= 0) return 0;
    
              std::cout << min << std::endl;
              for (int i = gas.size() - 1; i >= 0; i--) {
                  int rest = gas[i] - cost[i];
                  min += rest;
                  if (min >= 0) {
                      return i;
                  }
              }
              return -1;
          }
      };
      
    复制代码
  1. 两个维度权衡问题
  • 分发糖果问题

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果。 相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

问题的难点在于没法重新为孩子的评分排序,因为涉及到规则:相邻两个孩子评分更高的孩子会获得更多的糖果。那就要考虑左右孩子的评分。能不能逐一的去判断确定给每个孩子多少糖果呢? 应该要思考的是什么情况下需要考虑两种情况。左右两边的情况。先从第二个节点开始遍历,判断ratings[i] 比ratings[i - 1] 多的话,ratings[i]加1。但是当ratings是不断减小的,即右边的孩子比左边的孩子分数低,那么怎么确保左边的孩子得到的糖果比右边的孩子得到的糖果多呢?从右往左遍历,假如遇到左边的孩子得分比右边的孩子分数高,那么取max(candyVec[i], candyVec[i + 1] + 1)。最后统计结果。

  • 根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

因为[h,k]中的k是代表了前面比自己高的人有几个。因此先将身高从高到低排序,然后再依次根据k来安排他们的位置。

总的来说,难题还挺多的,套路不唯一,要想清楚怎么做,在什么情况应该怎么做。

第三天

这里总结9.11-9.13 日做的关于贪心算法中的区间问题,共三题:用最少数量的箭引爆气球、无重叠区间、划分字母区间。

  • 用最少数量的箭引爆气球

本质上这是寻找多个区间中,有多少组区间存在重叠,因为求出来多少组区间存在重叠就意味着可以最少用多少支箭来将其引爆。 1.想出来了先按照xstart 从小到大来进行排序。利用自己写的冒泡排序超时,自己写cmp利用sort函数来不会超时。2.排好序之后,如何进行累加求解重叠的区间呢?理解一点,在遍历的时候求最小右区间,因为和下一个区间的左区间大小关系决定了这两个区间是否有重叠关系,因此在根据左区间的大小排完序区间之后,依次遍历,更新最小右区间,当右区间比下一个区间的左区间小时,将箭的数量加一。

  • 无重叠区间

其实和上一个题类似,在上一个题目的基础上,求出来有几组区间存在重叠,那么就用总的区间个数减去组数,就能得出来,去掉几个区间就能保证剩下的区间没有重叠。

  • 划分字母区间

这道题目主要要理解两点:1.如何找到每个字母出现的最远区间。2. 为何要更新每个字母的最远出现的位置?第一个问题,利用hash[27]来进行赋值。2.因为只有那样,才能将区间尽可能多的分割开,并且每个字母都只出现一个区间里面。

感觉还没有太感觉出来在区间问题中的套路。继续总结吧,用心体会。


第四天

今天总结一下,合并区间(区间问题)、单调递增的数字(序列问题)、买卖股票的最佳时机含手续费(贪心解决股票问题)这三道题。

  • 合并区间

当时有想到的思路是,先排序。然后如何在遍历的时候完成需求,就有点儿乱了。没有想到如何更新结果中的右区间。实际上,在排完序之后,判断当前的区间左区间是否和前一个区间的右区间是否有重叠,如果没有重叠就依次把当前区间放进去,如果有重叠,那么把结果中最后一个区间的右区间赋值为 max(itervals[i][1], result.back()[1])。如何构建这个循环的过程需要多加练习。

  • 单调递增的数字

两个难点:1.关于字符串的操作 2.如何实现单调递增。

分析题目,找出小于或等于N的最大整数,能够理解,为什么从后向前遍历数字,遇到比当前数字大的时候,将前面的数字减1,当前数字变为9。但是这个减1和变9这个过程需要分开,如4132,当逻辑为:当2小于3,将3变为2,2变为9,没有问题,但是当遍历到size - 2 的时候,size - 3是1,size - 2 是2,符合递增,就不会进行变化的操作,但是遍历到size - 3 的时候就没有进行这个操作了。因此需要一个flag来记录从什么时候开始往后,都变为9。

字符串:从整数变为字符串:string strNum = to_string(N)

从字符串变为int stoi(strNum)

  • 买卖股票的最佳时机含手续费

和之前做过的一道买卖股票问题不同在于,这道题,在每次交易的时候都需要交手续费。贪心的思路是?一直没法绕过来的弯是,应该什么情况下决定要卖出去?明白利润区间这个概念,在利润区间中,到最后卖出,但问题是如何计算这其中的收益?因为假如模拟了每一次的交易,那么都交了手续费,但实际上假如到最后再卖出去,就交了一次手续费。因此就有了下面这几条语句:

// 来源:代码随想录
class Solution {
public:
    int maxProfit(vector& prices, int fee) {
        int result = 0;
        int minPrice = prices[0]; // 记录最低价格
        for (int i = 1; i < prices.size(); i++) {
            // 情况二:相当于买入
            if (prices[i] < minPrice) minPrice = prices[i];

            // 情况三:保持原有状态(因为此时买则不便宜,卖则亏本)
            if (prices[i] >= minPrice && prices[i] <= minPrice + fee) {
                continue;
            }

            // 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
            if (prices[i] > minPrice + fee) {
                result += prices[i] - minPrice - fee;
                minPrice = prices[i] - fee; // 情况一,这一步很关键
            }
        }
        return result;
    }
};

完成贪心算法的二刷,并对其进行了总结。继续加油!

你可能感兴趣的:(贪心算法,数据结构,算法)