代码随想录算法训练营第60期第三十天打卡
大家好,今天我们要走进一个全新的章节,这一章叫做贪心算法,前面我们讲的是回溯算法,那究竟什么是贪心算法呢?我们一起走进今天的内容。
第一部分贪心的理论基础
其实大家看这个名字估计也会有一定了解,贪心不就是想要最好的吗?我告诉大家贪心算法的本质就是由局部最优解找到全局最优解,这样说可能有一些抽象,比如说有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?很明显这十张钞票我每一次都拿目前金额最大的,那么十次之后我拿到的金额就是最大的,这就是局部最优可以推出全局最优。
那么我们上面的回溯算法或者是二叉树其实都是有模板的,那么我在这里很遗憾地告诉大家,我们的贪心算法是没有任何模板的,也就是我们要一个题一个思路,这里可能有一些题目会涉及到后面的动态规划,这里我们先不提动态规划,大家先要知道不一定所有的题都可以局部最优退出全局最优,有时候我们需要借助动态规划来处理,这里贪心可能会涉及到一些数学推导,这里大家不必关心,大家会写代码可以通过测试就可以,数学推导一般都非常复杂,那么说了这么多我们贪心的解题步骤是什么呢?
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
通过以上四步大家就可以找到合适的思路来解决贪心问题,贪心的理论基础我们就先说这么多,接下来我们就走进题目带大家感受以下贪心算法。
第一题对应力扣编号为455的题目分发饼干
这是我们贪心算法的入门题目,我们来看一下这道题目的具体要求:
我们一起来分析一下,我们的饼干要满足尺寸大于等于孩子的胃口,这样才可以满足这个孩子,我们需要满足尽可能多的孩子,并且我们求出这个最大数值,我们这里如何考虑呢?首先我们其实需要尽可能满足胃口大的孩子,因为胃口小的孩子我们很容易就满足了,只要满足尽可能多的胃口大的孩子这样我们就可以满足尽可能多的孩子,如果我们想满足满足胃口大的孩子我们就用最大尺寸的饼干去满足,否则我们用一个大尺寸的饼干去满足一个小胃口的孩子这很明显不会是最优方案,因此我们就会考虑把尺寸数组与胃口数组都排个序,这样我们考虑遍历孩子的胃口值,使用一个变量来存储目前饼干的数量,因为饼干一旦满足了一个孩子是要逐渐减1的,还要用一个变量来存储结果集,就是我们当前饼干的尺寸大于当前孩子的胃口时我们就让这个变量加1最后返回就可以,我们来看一下解题代码:
class Solution {
public:int findContentChildren(vector<int>& g, vector<int>& s) {sort(g.begin(), g.end());//胃口值sort(s.begin(),s.end());//饼干的尺寸int index = s.size() - 1;int result = 0;for (int i = g.size() - 1; i >= 0; --i){if (index >= 0 && s[index] >= g[i]){index--;result++;}}return result;}
};
大家刷题多了就会知道我们贪心算法经常是会对数组排序的,大家先有这个意识就可以,慢慢就会理解的,我们来看满足要求我们饼干数量要减减,我们的结果要加加,但是千万要满足我们还有饼干可以分给小朋友才可以。在这里我们其实也可以有另一个思路,就是小饼干优先喂饱小胃口,这样也是可以的,但是这样我们就需要遍历饼干而不是胃口了,解题代码我放到下面:
class Solution {
public:int findContentChildren(vector<int>& g, vector<int>& s) {sort(g.begin(),g.end());sort(s.begin(),s.end());int index = 0;for(int i = 0; i < s.size(); i++) { // 饼干if(index < g.size() && g[index] <= s[i]){ // 胃口index++;}}return index;}
};
这里我们还是index记录我们可以满足的孩子数,但是我们就要遍历饼干了,并且要从小到大遍历,这样我们到最后也可以找到可以满足的孩子数,但我更建议使用第一种方法,我感觉那一种方法可能更符合大家的思维。
第二题对应力扣编号为376的题目摆动序列
我们来到今天的第二题,其实一接触到这道题我根本就没有看出来这道题实在考察贪心算法,初步思考不像贪心算法,我们先看一下题目要求:
我们先要搞清楚题目的摆动序列是什么意思,就是后面的减前面的差值应该是一正一负才可以,当然题目特别强调了如果只有一个数或者两个不相等的数也可以算作摆动序列,最后我们需要返回给定数组中摆动序列的最长长度,我们思考一下这道题目我们应该如何解决?其实题目要求我们删除一些元素但是其他元素的相对位置是要保持不变的才可以,这道题目我们其实可以画一下图,就是数值大的数我们画的高一些,数值小的我们画的低一些,这样想必大家可以明白什么情况下是符合摆动序列的,其实就是一高一低,这样就会产生峰值,峰值越多其实摆动序列的长度就越长,代码随想录给出了我们一幅图帮助我们理解贪心究竟贪在哪里:
大家看我们就应该删除掉单调坡上的值,当然不包括峰值,这就是贪心策略了,峰值尽可能保持峰值,我们主要删除单调坡上的值,这道题我们需要考虑三种情况:
- 情况一:上下坡中有平坡
- 情况二:数组首尾两端
- 情况三:单调坡中有平坡
这几种情况我们需要重点考虑,具体的分析过程大家去代码随想录网站上去看一看,我在这里给大家说一下最后一种情况:
这道题目其实关键就是考虑平坡,尤其是单调中间有平坡,上下中间有平坡这个其实不好想,我先给出大家解题代码:
class Solution {
public: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 ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {result++;preDiff = curDiff; // 注意这里,只在摆动变化的时候更新prediff}}return result;}
};
大家要注意在平坡上我们不需要更新prediff,只有遇到摆动变化的时候才会更新,prediff与curdiff其实一个是记录前中两个元素的差值另一个是记录中后两个元素的差值,这个大家要注意,关键就是平坡如何处理,大家多去思考一下,还有一点我们的result要初始化为1,默认一个元素也算摆动序列,或者说默认最右边有一个峰值,接下来起初prediff是0我的curdiff可正可负,满足摆动序列条件,赋值,接下来如果发现curdiff符号没变说明遇到了单调坡我们不能赋值,这个值是不能算进摆动序列的,平坡也不可以算进去。这道题我就给大家讲这么多,大家一定要自己多思考。
第三题对应力扣编号为53的题目最大子序和
这道题似乎与上一道题目有类似的地方,我们就直接看一下题目:
其实这道题目有难点就是我们不知道这个连续子数组的长度,长度为多少只要不大于数组长度都是有可能的,其实这道题很容易就能看出是一道贪心算法,很明显我们要找的子数组里正数尽可能多负数尽可能少就是了,但是我们不能排序,因为我们的元素的相对位置是不可以改变的,我们就来看一下这道题目应该如何解决?
首先我们可以使用暴力的解法,就是双层循环,一层是起点另一层是终点,我们如果发现连续和大于当前的连续和我们就更新,否则我们就不更新,最后输出我们的连续和就可以,代码如下:
class Solution {
public:int maxSubArray(vector<int>& nums) {int result = INT32_MIN;int count = 0;for (int i = 0; i < nums.size(); ++i){count = 0;for (int j = i; j < nums.size(); ++j){count += nums[j];result = count > result ? count : result;}}return result;}
};
测试过了这是会超时的,大家不要用这种暴力解法,我们在这里讲解贪心方法,首先很明显我还是要有一个变量来保存连续和,当我们遍历到一个新的数的时候我们如果发现它前面所有元素的连续和是负的,那这种情况其实是不利于我们求出最大的子数组的和的,只会拖累我们后面的元素,那我们就重新从新元素开始继续,大家注意我们不是遇到负数就跳过,而是看连续和,这点要搞清楚,比如我们的样例1:[-2,-1,-3,4,-1,2,1,-5,4] 我们开始的连续和是-2我们不要了从-1开始我们当前的连续和还是负的,我们从-3开始连续和还是负的继续跳过,接下来是从4开始,这时候是正的,接下来是-1变成3还是正的,可以继续加,一直到了2,1得到6,接下来加上-5由于我们的连续和变小了所以我们不更新,这样其实就找到连续的和了,为啥我们不加后面的-5与4呢?因为我们加上了只会使得我们的和变小,负数的绝对值大于正数这时候我们不更新,我们来看一下代码如何写:
class Solution {
public:int maxSubArray(vector<int>& nums) {int result = INT32_MIN;int count = 0;for (int i = 0; i < nums.size(); i++) {count += nums[i];if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置)result = count;}if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和}return result;}
};
这是解题代码我就不多说了,上面其实解释的很详细,就是连续和为正的情况下我们才能继续加,否则就重新开始,而且只有越来越大的时候我们才会赋值,这道题目我就跟大家讲这么多。
今日总结
其实我觉得贪心算法还是很难的,今天这几道题都不算简单,一方面需要考虑的细节比较多,另一方面贪心算法没有固定的套路,我们每一道题都有不同的思路,大家平时都去思考,及时复习,今天的内容就分享到这里,我们明天见!