代码随想录算法训练营第31天|贪心算法理论基础、455. 分发饼干、376. 摆动序列、53. 最大子数组和

贪心算法理论基础

文章讲解
视频讲解

  • 贪心没有套路,说白了就是常识性推导加上举反例。
  • 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
  • 面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。

455. 分发饼干

题目链接:link
文章讲解:link
视频讲解:link

一、做题感受&第一想法

贪心:把小饼干分给小孩子,大饼干分给大孩子

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int num = 0;

        int i = 0,j = 0;
        while(i < g.size() && j < s.size()){
            if(s[j] >= g[i]){ //分发
                num ++;
                i++;
                j++;
            }
            else if(s[j] < g[i]){
                j++; //看下一个小饼干
            }
        }
        return num;
    }
};

二、学习文章后收获

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]) { // 遍历饼干
                result++;
                index--;
            }
        }
        return result;
    }
};

2.一个可能犯的错

  • 从小往大遍历 时,饼干和胃口怎么遍历都可以。
  • 从大往小遍历时,不能for循环遍历饼干,index控制胃口。否则可能被大胃口卡住!
    在这里插入图片描述

376. 摆动序列

题目链接:link
文章讲解:link
视频讲解:link

一、做题感受&第一想法

  • 错误思路:从第一个元素开始,寻找后续数组中首个大于第一个元素的元素,并把它记录为cur;之后寻找后续数组中首个小于cur的元素……统计得到的序列长度
  • 错误原因:会错过很多“小波动”(可以想象一下,如果是[1,2,5,4,5,4,5,4],第二个元素2是数组倒数第二小元素,上述方法就会一直等待一个比2小的元素来形成摆动,但是这样的话后续的所有波动就都记录不了了)
  • 所以,不能单纯地把“第一个出现波动的元素”作为判断基准!

记录一下错误代码(仅记录,这代码是错的!)

//仅记录,这是错误代码!!
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int result = 0,cur = nums[0],totalSize = 1;
        int i = 1;
        while(i < nums.size()){
            while(i < nums.size() && cur <= nums[i]){ //找到第一个比cur小的元素
                cout << "1" <<endl;
                i++;
            }
            if(i < nums.size() && cur > nums[i]){
                cur = nums[i];
                cout << nums[i] <<endl;
                totalSize++;
                i++;
            }
            while(i < nums.size() && cur >= nums[i]){ //找到第一个比cur大的元素
                i++;
            }
            if(i < nums.size() && cur < nums[i]){
                cur = nums[i];
                cout << nums[i] <<endl;
                totalSize++;
                i++;
            }
        }
        result = max(result,totalSize);
        cout << "total:" << totalSize <<endl;

        cur = nums[0];totalSize = 1;i = 1;
        while(i < nums.size()){
            while(i < nums.size() && cur >= nums[i]){ //找到第一个比cur大的元素
                // cout << "2" <<endl;
                i++;
            }
            // cout << "2.05" <<endl;
            if(i < nums.size() && cur < nums[i]){
                // cout << "2.1" <<endl;
                cur = nums[i];
                cout << nums[i] <<endl;
                totalSize++;
                i++;
            }
            while(i < nums.size() && cur <= nums[i]){ //找到第一个比cur小的元素
                i++;
            }
            if(i < nums.size() && cur > nums[i]){
                cur = nums[i];
                cout << nums[i] <<endl;
                totalSize++;
                i++;
            }
        }
        cout << "total:" << totalSize <<endl;
        result = max(result,totalSize);
        return result;
    }
};

二、学习文章后收获

1.贪心:思路分析

  • “折线图”思想:可以把求“摆动序列”看成求折线图的转折点。

    • 只要出现了一个转折点,就出现了一个“子摆动序列”!
    • 而没有转折的地方(上坡、下坡),不可能出现“子摆动序列”
      在这里插入图片描述
  • 关于“折线法”需要设定的(边界)条件

    • 针对转折点:找到一个“子摆动序列”,计数器++,相当于把坡顶点(局部最大值/最小值)加入了摆动序列
    • 针对开头元素:开头第一个“非平坡元素”,它天然地是摆动序列的元素([1,1,2,4,3]中的第二个1)
    • 针对结尾元素: 结尾元素天然地是摆动序列的元素([1,1,2,4,3]中的3)
  • 参数说明:

    • curDiff:nums[i+1] - nums[i],也就是后一个元素减去该元素。
    • preDiff:nums[i] - nums[i-1],也就是本元素减前一个元素。
    • 初值设定:preDiff = 0(可认为开头元素前有一个等值的虚拟元素,也就是一个虚拟的平坡,这样程序正好会把开头第一个“非平坡元素”识别成摆动序列的一部分。preDiff只有在最一开始为0),curDiff = 0(这个无所谓)
    • 参数变化:每个结点都计算自己的curDiff。只有当curDiff不为0且和preDiff一正一负(或者preDiff为0而curDiff不为0时,也就是刚经过开头)时,才把curDiff赋值给preDiff,更新坡度变化。(其实我们只关注preDiff的正负,因为它标明了当前/平坡之前,坡度是上坡还是下坡。)
  • 关于“折线法”需要考虑清楚的三种情况

    • 上坡->下坡:(如[3,4,2]中的4处)curDiff 和preDiff符号不同(一正一负)
    • 上坡->平坡->下坡:(如[2,3,3,3,1]中的最后一个3处)平坡处curDiff为0。下坡来临时,curDiff 和preDiff符号不同(一正一负)
    • 上坡->平坡->上坡:(如[1,3,3,3,4])无转折点!!不加入摆动序列。

2.贪心:代码

  • 卡哥的代码:
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums){
        int result = 0;
        int preDiff = 0, curDiff = 0;
        for(int i = 0;i < nums.size() - 1;i++){
            curDiff = nums[i+1] - nums[i];
            if((preDiff >= 0 && curDiff < 0) || (preDiff <= 0 && curDiff > 0)){
                preDiff = curDiff;
                result++;
            }
        }
        result++;
        return result;
    }
};
  • 另:我从评论区看到的方法:用数组记录前后的差值(也就是用数组表示两个元素之间是上坡下坡还是平坡,其实和卡哥的思路是一样的,只是实现上可以一次处理完整个数组)

3.动态规划(还没学,后续补上


53. 最大子数组和

题目链接:link
文章讲解:link
视频讲解:link

一、做题感受&第一想法

没有好想法。

二、学习文章后收获

1.“最大子数组和”问题思路

  • 贪心:如果已经收集的元素和sum是负的,那“不如不加”!直接重新从新元素开始收集!
  • 参数含义:
    • sum:元素和
    • result:已经找到的元素和的最大值,初值为INT_MIN

2.代码

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result = INT_MIN;
        int sum = 0;
        for(int i = 0;i < nums.size();i++){
            if(sum < 0){
                sum = nums[i];
            }
            else{
                sum += nums[i];
            }
            result = max(result,sum);
        }
        return result;
    }
};

3.动态规划

可以用动态规划,但是此处不写,后续博客会再练习到这题。

4.我个人的一些小思考

当sum为负数时,就从新收集元素,这难道不怕前一个元素是正数吗?

答:不会出现这样的情况。如果前一个元素是正数,而加了这个正数之后sum为负,说明在前一个元素(正数)时sum就已经为负了,于是会重新收集,矛盾。

所以“前一个元素是正数”的假设不成立!

三、过程中遇到的问题

1.INT_MIN的头文件

#include <limits.h>


  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值