【C++代码】分糖,分饼干,摇摆序列,贪心算法--代码随想录

  • 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。贪心算法一般分为如下四步:
    • 将问题分解为若干个子问题
    • 找出适合的贪心策略
    • 求解每一个子问题的最优解
    • 将局部最优解堆叠成全局最优解

题目:分发饼干

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

  • class Solution {
    public:
        int findContentChildren(vector<int>& g, vector<int>& s) {
            int g_p=g.size()-1,s_p=s.size()-1;
            sort(g.begin(),g.end());
            sort(s.begin(),s.end());
            int count = 0;
            while(s_p>-1 && g_p>-1){
                if(g[g_p]<=s[s_p]){
                    g_p--;
                    s_p--;
                    count++;
                }else{
                    g_p--;
                }
            }
            return count;
        }
    };
    
  • 为了满足更多的小孩,就不要造成饼干尺寸的浪费。大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。可以尝试使用贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。

  • 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,res=0;
            for(int i=g.size()-1;i>=0;i--){
                if(index<0){
                    break;
                }
                if(g[i]<=s[index]){
                    index--;
                    res++;
                }
            }
            return res;
        }
    };
    
  • 时间复杂度:O(nlogn);空间复杂度:O(1)。

  • 从代码中可以看出我用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。

题目:摆动序列

  • 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

  • class Solution {
    public:
        int wiggleMaxLength(vector<int>& nums) {
            if(nums.size()<2){
                return nums.size();
            }
            int curdiff=0;
            int prediff=0;
            int count=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)){
                    count++;
                    prediff=curdiff;//注意这里,只在摆动变化的时候更新prediff
                }
            }
            return count;
        }
    };
    
  • 本题异常情况的本质,就是要考虑平坡, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图:

    • 在这里插入图片描述
  • 时间复杂度:O(n);空间复杂度:O(1)

题目:最大子数组和

  • 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

  • 暴力解法的思路,第一层 for 就是设置起始位置,第二层 for 循环遍历数组寻找最大值

    • class Solution {
      public:
          int maxSubArray(vector<int>& nums) {
              int res = INT32_MIN;
              int count;
              for(int i=0;i<nums.size();i++){
                  count=0;
                  for(int j=i;j<nums.size();j++){
                      count += nums[j];
                      res = count>res?count:res;
                  }
              }
              return res;
          }
      };//超时
      class Solution {
      public:
          int maxSubArray(vector<int>& nums) {
              int res = INT32_MIN;
              int count;
              for(int i=0;i<nums.size();i++){
                  if(nums[i]<=0){
                      continue;
                  }
                  count=0;
                  for(int j=i;j<nums.size();j++){
                      count += nums[j];
                      res = count>res?count:res;
                  }
              }
              if(res==INT32_MIN){
                  res=nums[0];
                  for(int i=1;i<nums.size();i++){
                      res= res>nums[i]?res:nums[i];
                  }
              }
              return res;
          }
      };
      
  • 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。全局最优:选取最大“连续和”。从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。这相当于是暴力解法中的不断调整最大子序和区间的起始位置

  • class Solution {
    public:
        int maxSubArray(vector<int>& nums) {
            int res=INT32_MIN;
            int count=0;
            for(int i=0;i<nums.size();i++){
                count+=nums[i];
                if(count>res){
                    res=count;
                }
                if(count<=0){
                    count=0;
                }
            }
            return res;
        }
    };
    
  • 时间复杂度:O(n);空间复杂度:O(1)

题目:买卖股票的最佳时机 II

  • 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润

  • 选一个低的买入,再选个高的卖,再选一个低的买入…循环反复。局部最优:收集每天的正利润,全局最优:求得最大利润

  • class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int res=0;
            for(int i=0;i<prices.size()-1;i++){
                res += (prices[i+1]-prices[i]) > 0?(prices[i+1]-prices[i]):0;
            }
            return res;
        }
    };
    

题目:跳跃游戏

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

  • 不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。这个范围内,别管是怎么跳的,反正一定可以跳过来。**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!**每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

  • 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

  • class Solution {
    public:
        bool canJump(vector<int>& nums) {
            int cover=0;
            if(nums.size()<=1) 
                return true;
            for(int i=0;i<=cover;i++){
                cover=max(i+nums[i],cover);
                if(cover>=nums.size()-1){
                    return true;
                }
            }
            return false;
        }
    };
    
  • 不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。

题目:跳跃游戏 II

  • 给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]。每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:0 <= j <= nums[i] ;i + j < n。返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

  • 贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。**真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!**如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点

  • 移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。

  • 如果当前覆盖最远距离下标不是集合终点,步数就加一,还需要继续走。如果当前覆盖最远距离下标就是集合终点,步数不用加一,因为不能再往后走了。

  • class Solution {
    public:
        int jump(vector<int>& nums) {
            if(nums.size()<=1)
                return 0;
            int max_len=0;// 当前覆盖的最大距离
            int count = 0;
            int next_len=0;// 下一步覆盖最远距离下标
            for(int i=0;i<nums.size();i++){
                next_len=max(nums[i]+i,next_len);// 更新下一步覆盖最远距离下标
                if(i==max_len){ // 遇到当前覆盖最远距离下标
                    count++;
                    max_len=next_len;
                    if(next_len>=nums.size()-1)
                        break;
                }
            }
            return count;
        }
    };
    
  • 时间复杂度: O(n);空间复杂度: O(1)。理解本题的关键在于:以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点,这个范围内最少步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。

题目:K 次取反后最大化的数组和

  • 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好 k 次。可以多次选择同一个下标 i 。以这种方式修改数组后,返回数组 可能的最大和

  • 贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。

  • 那么本题的解题步骤为:

    • 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
    • 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
    • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
    • 第四步:求和
  • 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(k>0 && nums[i]<0){
                    nums[i] = -nums[i];
                    k--;
                }
            }
            if(k%2==1) nums[nums.size()-1] = -nums[nums.size()-1];
            int res=0;
            for(int item:nums){
                res+=item;
            }
            return res;
        }
    };
    

题目:加油站

  • 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

  • 暴力的方法很明显就是 O ( n 2 ) O(n^2) O(n2) 的,遍历每一个加油站为起点的情况,模拟一圈。如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while

  • class Solution {
    public:
        int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
            for(int i=0;i<cost.size();i++){
                int rest = gas[i]-cost[i];
                int index=(i+1)%cost.size();
                while(rest>0&&index!=i){// 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了)
                    rest+= gas[index]-cost[index];
                    index=(index+1)%cost.size();
                }
                // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
                if(rest>=0 && index==i)
                    return i;
            }
            return -1;
        }
    };//超时
    
  • 直接从全局进行贪心选择,情况如下:

    • 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
    • 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
    • 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点
  • class Solution {
    public:
        int canCompleteCircuit(vector<int>& gas, vector<int>& 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;
            for(int i=gas.size()-1;i>=0;i--){
                int rest=gas[i]-cost[i];
                min += rest;
                if(min>=0){
                    return i;
                }
            }
            return -1;
        }
    };
    
  • 首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。

  • 那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置

  • class Solution {
    public:
        int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
            int cursum=0;
            int tosum =0;
            int start=0;
            for(int i=0;i<gas.size();i++){
                cursum+=gas[i]-cost[i];
                tosum+=gas[i]-cost[i];
                if(cursum<0){ // 当前累加rest[i]和 curSum一旦小于0
                    start=i+1;// 起始位置更新为i+1
                    cursum=0;// curSum从0开始
                }
            }
            if(tosum<0) return -1;
            return start;
        }
    };
    
  • 时间复杂度:O(n);空间复杂度:O(1)

题目:分发糖果

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

  • 这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

  • class Solution {
    public:
        int candy(vector<int>& ratings) {
            vector<int> res_vec(ratings.size(),1);
            for(int i=1;i<ratings.size();i++){
                if(ratings[i]>ratings[i-1]){
                    res_vec[i] += res_vec[i-1];
                }
            }
            for(int i=ratings.size()-2;i>=0;i--){
                if(ratings[i]>ratings[i+1]){
                    res_vec[i] = max(res_vec[i+1]+1,res_vec[i]);
                }
            }
            int res=0;
            for(int res_one:res_vec){
                res+=res_one;
            }
            return res;
        }
    };
    
  • 时间复杂度: O(n);空间复杂度: O(n)

  • 那么本题我采用了两次贪心的策略:一次是从左到右遍历,只比较右边孩子评分比左边大的情况。一次是从右到左遍历,只比较左边孩子评分比右边大的情况。这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。

题目:柠檬水找零

  • 在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。注意,一开始你手头没有任何零钱。给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

  • class Solution {
    public:
        bool lemonadeChange(vector<int>& bills) {
            int count5=0,count10=0;
            for(int i=0;i<bills.size();i++){
                if(bills[i]==5) count5++;
                if(bills[i]==10){
                    if(count5<=0) return false;  
                    count10++;
                    count5--;
                }
                if(bills[i]==20){
                    if(count10>0&&count5>0){
                        count10--;
                        count5--;
                    }else if(count5>=3){
                        count5-=3;
                    }else return false;
                }
            }
            return true;
        }
    };
    
  • 只需要维护三种金额的数量,5,10和20。有如下三种情况:

    • 情况一:账单是5,直接收下。
    • 情况二:账单是10,消耗一个5,增加一个10
    • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
  • 所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。

题目:根据身高重建队列

  • 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

  • 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

  • 如果两个维度一起考虑一定会顾此失彼。如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!

  • 按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。

  • 所以在按照身高从大到小排序后:局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性全局最优:最后都做完插入操作,整个队列满足题目队列属性

  • class Solution {
    public:
        static bool cmp(const vector<int>& a,const vector<int>& b){
            if(a[0]==b[0]) return a[1]<b[1];
            return a[0]>b[0];
        }
        vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
            sort(people.begin(),people.end(),cmp);
            vector<vector<int>> res;
            for(int i=0;i<people.size();i++){
                int position = people[i][1];
                res.insert(res.begin()+position,people[i]);
            }
            return res;
        }
    };
    

vector& b){
if(a[0]==b[0]) return a[1]<b[1];
return a[0]>b[0];
}
vector<vector> reconstructQueue(vector<vector>& people) {
sort(people.begin(),people.end(),cmp);
vector<vector> res;
for(int i=0;i<people.size();i++){
int position = people[i][1];
res.insert(res.begin()+position,people[i]);
}
return res;
}
};




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羞儿

写作是兴趣,打赏看心情

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值