Day27 贪心算法:理论基础 455.分发饼干 376.摆动序列 53.最大子序和

144 阅读4分钟

理论基础

贪心算法其实就是没有什么规律可言,所以大家了解贪心算法 就了解它没有规律的本质就够了。

不用花心思去研究其规律,没有思路就立刻看题解。

基本贪心的题目,有两个极端:要不就是特简单,要不就是死活想不出来。

学完贪心之后再去看动态规划,就会了解贪心和动规的区别。

🦄个人总结:局部最优做法全局最优目的

455.分发饼干

题目链接:455.分发饼干

难度指数:😀😐

可以尝试使用贪心策略,先将饼干数组和小孩数组排序

大饼干先喂饱大胃口:

27.01.png

时间复杂度:O(nlogn)

空间复杂度:O(1)

AC代码: (核心代码模式)

 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 >= 0 控制index不取负 (即保证不会数组越界)
                 result++;
                 index--;
             }
         }
         return result;
     }
 };


也可以,

小饼干先喂饱小胃口:

这个思考方式更好一些。

因为用小饼干优先喂饱小胃口的 这样可以尽量保证最后省下来的是大饼干(虽然题目没有这个要求)!

所以还是小饼干优先先喂饱小胃口更好一些,也比较直观。

AC代码: (核心代码模式)

 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 < g.size()  控制不会数组越界
                 index++;
             }
         }
         return index;
     }
 };

376.摆动序列

题目链接:376.摆动序列

难度指数:😀😐😕

思路1(贪心解法):

要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

题目要求删除元素使其达到最大摆动序列:

27.02.png

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

局部最优推出全局最优,并且举不出反例,就可以试试贪心

27.03.png

比如:序列 [2,5] ,它的峰值数量是2,如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。

  • curDiff
  • preDiff

内心OS:怎么感觉有点双指针的意思。

时间复杂度:O(n)

空间复杂度:O(1)

AC代码: (核心代码模式)

 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 ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
                 result++;
                 preDiff = curDiff;
             } 
         }
         return result;
     }
 };

总结:

在计算峰值的时候,还是有一些代码技巧的,例如序列两端的峰值如何处理。

这些技巧,其实还是要多看多用才会掌握。

思路2(动态规划):

53.最大子序和

题目链接:53.最大子序和

难度指数:😀😕🙁

其实这道题目是一道动态规划的题目。

暴力解法:

第一层 for循环 就是设置起始位置,第二层 for循环 遍历数组寻找最大值

时间复杂度:O(n^2)

空间复杂度:O(1)

AC代码: (核心代码模式)

 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++) {  //每次从起始位置i开始遍历寻找最大值
                 count += nums[j];
                 result = count > result ? count : result;
             }
         }
         return result;
     }
 };

力扣增强了数据,现在写暴力会超时。

贪心解法:

动画:

27.04.gif

红色的起始位置就是贪心每次取count为正数的时候,开始一个区间的统计。

AC代码: (核心代码模式)

 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;
     }
 };

贪心的思路为局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。从而推出全局最优:选取最大“连续和”

代码很简单,但是思路却比较难。还需要反复琢磨。

总结:

Carl:

不少同学都来问,如果数组全是负数这个代码就有问题了,如果数组里有int最小值这个代码就有问题了。

大家不要脑洞模拟哈,可以亲自构造一些测试数据试一试,就发现其实没有问题。

数组都为负数,result记录的就是最小的负数,如果数组里有int最小值,那么最终result就是int最小值。

动态规划: