C++学习笔记-贪心算法

资料来源:代码随想录

1.贪心算法理论基础

贪心的本质是选择每一阶段的局部最优,从而达到全局最优

刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心

贪心算法一般分为如下四步:

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

2.分发饼干 455

有两种解法:第一种是尽可能用大饼干喂饱胃口大的小孩,遍历顺序就是从后往前遍历;第二种是尽可能用小饼干喂饱胃口小的小孩,遍历顺序就是从前往后遍历。基本思想都是一样的,但第二种是先遍历饼干再遍历小孩。

注意,小孩必须被放在外层的for循环里,因为如果饼干都不满足这个小孩的话,就要跳过这个小孩,但饼干不能一直被跳过,否则最后一个都喂不饱。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());  //先从小到大排序,后面从后往前遍历
        sort(s.begin(),s.end());
        int result=0; //喂饱的小孩数量
        int index=s.size()-1;  //饼干的起始下标

        for(int i=g.size()-1; i>=0; i--)  //小孩从后往前遍历
        {
            if(index>=0 && s[index]>=g[i])  //如果饼干能够喂饱小孩。注意必须先判断Index不为负数
            {
                result++;  //喂饱小孩数量+1
                index--;  //这个饼干喂小孩了,就往前移动一个
            }
        }
        return result;
    }
};

3.摆动序列 376(别看代码随想录)

本题求摆动序列的长度,即摆动序列里元素的个数。可以统计原序列中相邻两元素之间差值的正负交替次数,最后再+1,就是摆动序列里元素的个数。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size()<=1) return nums.size();

        int up=1;
        int down=1;
        for(int i=1; i<nums.size(); i++)
        {
            if(nums[i]>nums[i-1])
            {
                up=down+1;
            }
            if(nums[i]<nums[i-1])
            {
                down=up+1;
            }
        }
        return max(up,down);

    }
};

4.最大子数组和 53

用count来做数值累加,用result来记录过程中的最大值。

注意当count变成负数的时候,它对后面数据的累加一定只会起到降低总和的效果,这时候就不能要现在这个count了,直接置0,等于从下一个元素开始重新累加count。但只要count还是正数,哪怕当前累加的元素是负的,它都可以对后面的累加起到增大总和的作用,所以这个时候还是可以继续使用现在这个count的。不用担心当前元素为负会导致错过最大总和,因为还用result来记录过程中的最大累加值了。数组里全是负数的话代码也没有问题,count每次都会被清零,但result会记录下最小的负数。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result=INT_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;
    }
};

5.买卖股票的最佳时机II 122

每天都可以买卖股票,所以每天都可以产生利润(除了第一天)。所以可以计算出每一天能产生的利润,把所有正利润加起来,就可以获得最大利润了。因为要求的是最大利润,所以不用关心这些利润都是在哪个区间产生的。

局部最优:只收集每天的正利润;全局最优:获得最大利润。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result=0;

        for(int i=1; i<prices.size(); i++)
        {
            result+=max(prices[i]-prices[i-1],0); //确保只累加正利润
        }

        return result;
    }
};

6.跳跃游戏 55

怎么跳的不重要,关键是看跳跃的覆盖范围能不能覆盖到终点。

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

局部最优:每次跳跃取最大覆盖范围;全局最优:得到整体的最远覆盖范围,看能否覆盖到终点。

i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。

而 cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)。

如果 cover 大于等于了终点下标,直接 return true 就可以了。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        if(nums.size()==1) return true;  //只有一个元素的话必能到达最后一个下标
        int cover=0;   //可覆盖范围
        for(int i=0; i<=cover; i++)  //i只能在cover的范围内移动
        {
            cover=max(i+nums[i],cover);  //cover不断在当前元素的基础上扩充
            //判断要写在循环里面,因为未必需要循环完才能到最后一个下标
            if(cover>=nums.size()-1) return true;
        }

        //走完整个数组了还没有能cover到最后一个下标,说明不可以
        return false;
        
    }
};

7.跳跃游戏II 45

for循环逻辑:每到一个元素,就要更新站在这个元素上,下一步能到达的最远距离范围nextDistance。i一直在++,如果i没到当前能够到达的最远距离范围curDistance,就暂且让它先自增,不做其它处理;如果已经到了当前能够到达的最远距离范围,但还没有到终点,那就说明现在这个不够了,必须要再往前迈一步。“往前迈一步”这个动作不是由i完成的,而是令记录步数的ans+1、并更新当前能够到达的最远距离范围,也就是之前记录的下一步能够到达的最远距离范围。更新之后的当前能够到的最远距离范围如果涵盖了末尾元素,说明可以跳到末尾。而“走到最远距离范围再往前迈一步”的操作也保证了使用最小步数。

class Solution {
public:
    int jump(vector<int>& nums) {
        if(nums.size()==1) return 0;

        int ans=0;  //记录最小步数
        int curDistance=0;  //当前能够到达的最远距离范围
        int nextDistance=0;  //下一步能够到达的最远距离范围

        for(int i=0; i<nums.size(); i++)
        {
            nextDistance=max(nums[i]+i,nextDistance);  //每到一个元素,就更新下一步能够到达的最远距离范围
            if(i==curDistance) //i走到当前能够到达的最远距离
            {
                ans++;   //这两行是“往前一步”的操作
                curDistance=nextDistance;
                if(curDistance>=nums.size()-1)  break;  //当前能够到达的最远范围包含终点,直接结束循环
            }
        }
        return ans;
    }
};

8.K次取反后的最大化数组和 1005

第一次贪心:

局部最优:把绝对值最大的负数取反为正数,使该数值达到最大

全局最优:每个数值都最大后,数组和能够达到最大

第二次贪心:以上过程走完一遍之后K还没消耗完

局部最优:此时已经是一个正整数序列,所以把最小的正整数不断取反,消耗K

全局最优:可能为负的是最小的那个数,所以数组和能够达到最大

步骤:

1.按照绝对值的大小,对数组进行从大到小排序;

2.从前到后,把所有负数进行取反,每取反一次,K--;

3.如果K还没被消耗完,则对最小的数反复取反消耗K,注意这里并不是非要把“不断取反”的过程体现出来,可以简化一下,比如如果K现在是奇数,那么反复取反等于只取反一次,如果是偶数,那么反复取反等于没操作,可以直接跳过这个过程;

4.对数组求和。

class Solution {
public:
    static bool cmp(int a, int b)
    {
        return abs(a)>abs(b);
    }

    int largestSumAfterKNegations(vector<int>& nums, int K) {
        sort(nums.begin(),nums.end(),cmp);

        for(int i=0; i<nums.size(); i++)
        {
            if(nums[i]<0 && K>0)
            {
                nums[i]*=-1;
                K--;
            }
        }

        if(K%2==1) //K是奇数
        {
            nums[nums.size()-1]*=-1;  //这就是对最小整数不断取反的过程
        }
        int result=0;
        for(int a:nums)
        {
            result+=a;
        }
        return result;
    }
};

10.加油站 134

如果整体的耗油量>加油量,那必然不能跑完一圈。

每个加油站点i的剩余油量为rest[i]=gas[i]-cost[i],i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再次从0开始计算curSum。

局部最优:rest[i]的累加和curSum一旦小于0,说明i及之前的都不能支撑完跑一圈,那么起始位置从i+1重新开始,curSum从0重新开始。

全局最优:找到能跑一圈的起始位置。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int start=0;  //起始位置
        int curSum=0;  //会更新的rest累加和
        int totalSum=0;  //rest总和

        for(int i=0; i<gas.size(); i++)
        {
            curSum+=gas[i]-cost[i];
            totalSum+=gas[i]-cost[i];

            if(curSum<0)
            {
                start=i+1;
                curSum=0;
            }
        }
        if(totalSum<0) return -1;  //总体加油量小于耗油量,说明必不可以走完一圈

        return start;
    }
};

11.分发糖果 135

既要比较每个孩子分数和左边的大小,也要比较和右边的大小。所以本题重点在于有两个循环,一个从左到右遍历,得到右>左的结果;一个从右到左遍历,得到左>右的结果。

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candy(ratings.size(),1);  //建立和ratings一样大小的数组,初始值为1,保证每个孩子都有一个糖果

        //从左向右遍历,找出右>左的,是i和i-1比较,所以从i=1开始
        for(int i=1; i<ratings.size(); i++)
        {
            if(ratings[i]>ratings[i-1])
            {
                candy[i]=candy[i-1]+1;
            }
        }

        //从右向左遍历,找出左>右的,是i和i+1比较,所以从倒数第二个开始
        for(int i=ratings.size()-2; i>=0; i--)
        {
            if(ratings[i]>ratings[i+1])
            {
                candy[i]=max(candy[i+1]+1,candy[i]); //第二个candy[i]是上面从左向右遍历时得到的结果
            }
        }

        //收集结果
        int result=0;
        for(int i=0; i<ratings.size(); i++)
        {
            result+=candy[i];
        }

        return result;
    }
};

12.柠檬水找零 860

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five=0, ten=0, twenty=0;

        for(int bill : bills)
        {
            //情况1:给的是5美元,不需要找零
            if(bill==5)
            {
                five++;
            }

            //情况2:给的是10美元,需要找5美元,所以要做能否找零的判断
            if(bill==10)
            {
                if(five<=0) return false;
                five--;
                ten++;
            }

            //情况3:给的是20,优先用10找零,因为5用处更多
            if(bill==20)
            {
                if(five>0 && ten>0)
                {
                    five--;
                    ten--;
                }
                else if(five>=3)
                {
                    five=five-3;
                }
                else
                {
                    return false;  //找不开了
                }
            }
        }
        return true;
    }
};

13.根据身高重建队列 406

二维数组,所以排序也有两个维度:h和k。有两个维度的时候,一定要分别考虑,否则容易顾此失彼

class Solution {
public:
    static bool cmp(vector<int>& a, vector<int>& b)
    {
        if(a[0]==b[0]) return a[1]<b[1];  //身高相同的时候,按照k从小到大排序
        return a[0]>b[0];  //否则按照身高从大到小排序
    }

    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(),people.end(),cmp);  //先按照要求给原二维数组排序
        vector<vector<int>> que;  //定义一个新数组放结果

        //排好序后,就该按照k来插入调整位置了
        for(int i=0; i<people.size(); i++)
        {
            int position=people[i][1];  //插入位置的下标就是k
            que.insert(que.begin()+position,people[i]);
        }

        return que;
    }
};

这部分先学到这里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值