代码随想录-贪心专题

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

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

        1、将问题分解为若干个子问题

        2、找出适合的贪心策略

        3、求解每一个子问题的最优解

        4、将局部最优解堆叠成全局最优解

455.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

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

示例  1:

  • 输入: g = [1,2,3], s = [1,1]
  • 输出: 1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以你应该输出 1。

        思路:每次用最大的饼干去满足胃口比较大的孩子 ,这样才能尽量多地满足孩子的需求。因此将两个数组进行排序,然后必须遍历孩子的胃口,用一个指针指向饼干,一旦能够满足当前孩子的胃口,就进行更新ans。

int cmp(int *a,int* b){
    return *a-*b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize) {
    int ans=0;
    qsort(g,gSize,sizeof(int),cmp);
    qsort(s,sSize,sizeof(int),cmp);
    int idx=sSize-1;
    //遍历孩子的胃口值
    for(int i=gSize-1;i>=0;--i){
        if(idx>=0&&g[i]<=s[idx]){
            //饼干能够满足孩子胃口
            ans++;
            idx--;
        }
    }
    return ans;
}

376. 摆动序列 

 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3)  是正负交替出现的。相反, [1,4,7,2,5]  和  [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

  • 输入: [1,7,4,9,2,5]
  • 输出: 6
  • 解释: 整个序列均为摆动序列。

        本题异常情况的本质,就是要考虑平坡, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡。

int wiggleMaxLength(int* nums, int numsSize){
    if(numsSize<2) return numsSize;
    int predef=0;//前一对差值
    int curdef=0;//当前差值
    int ans=1;//记录峰值个数,序列默认序列最右边有一个峰值
    for(int i=0;i<numsSize-1;++i){
        curdef=nums[i+1]-nums[i];
        if((predef>=0&&curdef<0)||(predef<=0&&curdef>0)){
            ans++;
            predef=curdef;//只有当出现摆动时才会更新predef
        }
    }
    return ans;
}

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

  • 输入: [-2,1,-3,4,-1,2,1,-5,4]
  • 输出: 6
  • 解释:  连续子数组  [4,-1,2,1] 的和最大,为  6。

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

int maxSubArray(int* nums, int numsSize) {
    int mmax=INT_MIN;
    int cur=0;//存放当前结果
    for(int i=0;i<numsSize;++i){
        cur+=nums[i];
        //当局部结果大于mmax时更新结果
        if(cur>mmax){
            mmax=cur;
        }
        //当局部结果为负时,对最终结果无益,重新开始计算
        if(cur<0){
            cur=0;
        }
    }
    return mmax;
}

         动态规划:用dp[i]表示[0,i]的连续子序列之和的最大值。

int maxSubArray(int* nums, int numsSize) {
    if(numsSize==0) return 0;
    int dp[numsSize];//表示i之前的最大连续子数组
    dp[0]=nums[0];
    int ans=dp[0];
    for(int i=1;i<numsSize;++i){
        dp[i]=fmax(dp[i-1]+nums[i],nums[i]);//状态转移
        if(dp[i]>ans) ans=dp[i];
    }
    return ans;
}

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

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7

         贪心思想:检查相邻的两个价格之间是否能够得到利润,如果不能得到利润就不加上差值,这样就能得到最大利润。局部最优:只收集每天的正利润,全局最优:得到最大利润

int maxProfit(int* prices, int pricesSize) {
    int ans=0;
    for(int i=1;i<pricesSize;++i){
        int cur=prices[i]-prices[i-1];
        if(cur>0) ans+=cur;
    }
    return ans;
}

        动态规划:dp[i][2]其中0表示买入,1表示卖出,状态转移需要考虑买与不买两种情况。

int maxProfit(int* prices, int pricesSize) {
    int n=pricesSize;
    int dp[n][2];
    dp[0][0]=0;
    dp[0][1]=-prices[0];
    for(int i=1;i<n;++i){
        dp[i][0]=fmax(dp[i-1][0],dp[i-1][1]+prices[i]);
        dp[i][1]=fmax(dp[i-1][1],dp[i-1][0]-prices[i]);
    }
    return dp[n-1][0];
}

55. 跳跃游戏 

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例  1:

  • 输入: [2,3,1,1,4]
  • 输出: true
  • 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

        本题贪心的关键是:不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。 

bool canJump(int* nums, int numsSize) {
    int cover=0;
    for(int i=0;i<=cover;++i){//i的值不能超过cover
        cover=fmax(i+nums[i],cover);
        if(cover>=numsSize-1) return true;
    }
    return false;
}

45.跳跃游戏 II 

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:

  • 输入: [2,3,1,1,4]
  • 输出: 2
  • 解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳  1  步,然后跳  3  步到达数组的最后一个位置。

        以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点, 要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数。

        那么局部最优:求当前这步的最大覆盖,那么尽可能多走,到达覆盖范围的终点,只需要一步。整体最优:达到终点,步数最少。这里需要统计两个覆盖范围,当前这一步的最大覆盖下一步最大覆盖

int jump(int* nums, int numsSize) {
    if(numsSize==1) return 0;
    int ans=0;
    int curDis=0;//当前最远距离的下标
    int nextDis=0;//下一步最远距离的下标
    for(int i=0;i<numsSize;++i){
        nextDis=fmax(i+nums[i],nextDis);
        if(i==curDis){//遇到当前覆盖最远距离下标
            ans++;
            curDis=nextDis;
            if(nextDis>=numsSize-1) break;
        }
    }
    return ans;
}

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

给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)

以这种方式修改数组后,返回数组可能的最大和。

示例 1:

  • 输入:A = [4,2,3], K = 1
  • 输出:5
  • 解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。

        第一次贪心:局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。处理之后,如果K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。

        第二次贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和达到最大。

int cmp(int* a,int* b){
    return abs(*b)-abs(*a);
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
    //注意排序的顺序,要按照绝对值的顺序从大到小排序
    qsort(nums,numsSize,sizeof(int),cmp);
    //每次从绝对值最大的值开始逆转
    for(int i=0;i<numsSize;++i){
        if(nums[i]<0&&k>0){
            nums[i]=nums[i]*(-1);
            k--;
        }
    }
    //若遍历完数组后k还有剩余(此时所有元素应均为正),并且k为奇数时,则将绝对值最小的元素nums[numsSize-1]变为负
    if(k%2==1){
        nums[numsSize-1]*=-1;
    }
    int sum=0;
    for(int i=0;i<numsSize;++i){
        sum+=nums[i];
    } 
    return sum;
}

 134. 加油站

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

示例 1: 输入:

  • gas = [1,2,3,4,5]
  • cost = [3,4,5,1,2]

        暴力法:枚举起始节点,然后模拟跑一圈的过程,检查是否能够回到起始节点。此法超时。

int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
    for(int i=0;i<gasSize;++i){
        int rest=gas[i]-cost[i];
        int index=(i+1)%costSize;
        while(rest>0&&index!=i){
            rest=rest-cost[index]+gas[index];
            index=(index+1)%costSize;
        }
        if(rest>=0&&index==i) return index;
    }
    return -1;
}

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

int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
    int cursum=0;
    int totalsum=0;
    int start=0;
    for(int i=0;i<gasSize;++i){
        cursum+=gas[i]-cost[i];
        totalsum+=gas[i]-cost[i];
        if (cursum<0) {   // 当前累加rest[i]和 curSum一旦小于0
            start=i+1;  // 起始位置更新为i+1
            cursum=0;     // curSum从0开始
        }
    }
    if(totalsum<0) return -1;
    return start;
}

135. 分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

示例 1:

输入:ratings = [1,0,2]
输出:5

        先确定右边评分大于左边的情况(也就是从前向后遍历)

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。

         再确定左孩子大于右孩子的情况(从后向前遍历)

局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多

int candy(int* ratings, int ratingsSize) {
    int candysize[ratingsSize];
    for(int i=0;i<ratingsSize;++i){
        candysize[i]=1;
    }
    //检查右边是否比左边高
    for(int i=1;i<ratingsSize;++i){
        if(ratings[i]>ratings[i-1]){
            candysize[i]=candysize[i-1]+1;
        }
    }
    //检查左边是否比右边高
    for(int i=ratingsSize-2;i>=0;--i){
        if(ratings[i]>ratings[i+1]){
            candysize[i]=fmax(candysize[i+1]+1,candysize[i]);
        }
    }
    int res=0;
    for(int i=0;i<ratingsSize;++i){
        res+=candysize[i];
    }
    return res;
}

860.柠檬水找零 

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:

  • 输入:[5,5,5,10,20]
  • 输出:true

        局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。 

bool lemonadeChange(int* bills, int billsSize) {
    int five=0,ten=0,twenty=0;
    for(int i=0;i<billsSize;++i){
        if(bills[i]==5){
            five++;
        }else if(bills[i]==10){
            ten++;
            if(five>0){
               five--; 
            }else{
               return false; 
            }
        }else{
            twenty++;
            if(ten>0&&five>0){
                ten--;
                five--;
            }else if(five>=3){
                five-=3;
            }else{
                return false;
            }
        }
    }
    return true;
}

406.根据身高重建队列 

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

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

示例 1:

  • 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
  • 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

所以在按照身高从大到小排序后:

局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

全局最优:最后都做完插入操作,整个队列满足题目队列属性

class Solution {
public:
    // 身高从大到小排(身高相同k小的站前面)
    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);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
};

452. 用最少数量的箭引爆气球 

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

示例 1:

  • 输入:points = [[10,16],[2,8],[1,6],[7,12]]
  • 输出:2
class Solution {
private:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0];
    }
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.size() == 0) return 0;
        sort(points.begin(), points.end(), cmp);

        int result = 1; // points 不为空至少需要一支箭
        for (int i = 1; i < points.size(); i++) {
            if (points[i][0] > points[i-1][1]) {  // 气球i和气球i-1不挨着,注意这里不是>=
                result++; // 需要一支箭
            }
            else {  // 气球i和气球i-1挨着
                points[i][1] = min(points[i-1][1], points[i][1]); // 更新重叠气球最小右边界
            }
        }
        return result;
    }
};

435. 无重叠区间 

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

示例 1:

  • 输入: [ [1,2], [2,3], [3,4], [1,3] ]
  • 输出: 1
  • 解释: 移除 [1,3] 后,剩下的区间没有重叠。
class Solution {
public:
    // 按照区间右边界排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 1; // 记录非交叉区间的个数
        int end = intervals[0][1]; // 记录区间分割点
        for (int i = 1; i < intervals.size(); i++) {
            if (end <= intervals[i][0]) {
                end = intervals[i][1];
                count++;
            }
        }
        return intervals.size() - count;
    }
};

763.划分字母区间

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

  • 输入:S = "ababcbacadefegdehijhklij"
  • 输出:[9,7,8] 解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
class Solution {
public:
    vector<int> partitionLabels(string s) {
        int hash[27]={0};
        int n=s.size();
        //记录每个字母最后一次出现的位置
        for(int i=0;i<n;++i){
            hash[s[i]-'a']=i;
        }
        int left=0;
        int right=0;
        vector<int> res;
        for(int i=0;i<n;++i){
            right=max(right,hash[s[i]-'a']);//找到每个字母最后的一个位置
            if(i==right){
                res.push_back(right-left+1);
                left=i+1;
            }
        }
        return res;
    }
};

56. 合并区间

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

  • 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
  • 输出: [[1,6],[8,10],[15,18]]
  • 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;
        if (intervals.size() == 0) return result; // 区间集合为空直接返回
        // 排序的参数使用了lambda表达式
        sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});

        // 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并
        result.push_back(intervals[0]); 

        for (int i = 1; i < intervals.size(); i++) {
            if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间
                // 合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的
                result.back()[1] = max(result.back()[1], intervals[i][1]); 
            } else {
                result.push_back(intervals[i]); // 区间不重叠 
            }
        }
        return result;
    }
};

738.单调递增的数字

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

  • 输入: N = 10
  • 输出: 9
  • 输入: N = 1234
  • 输出: 1234

        暴力解:从大到小遍历判断

bool check(int n){
    int mmax=10;
    while(n){
        int cur=n%10;
        if(mmax>=cur) mmax=cur;
        else return false;
        n/=10;
    }
    return true;
}
int monotoneIncreasingDigits(int n) {
    for(int i=n;i>=0;--i){
        if(check(i)){
            return i;
        }
    }
    return -1;
}

        例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。

想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。

最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。

int monotoneIncreasingDigits(int n) {
    char s[12];
    sprintf(s,"%d",n);
    int l=strlen(s);
    // flag用来标记赋值9从哪里开始
    // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
    int flag=l;
    for(int i=l-1;i>0;--i){
        //找到第一个非递增的下标,并
        if(s[i-1]>s[i]){
            flag=i;
            s[i-1]--;
        }
    }
    for(int i=flag;i<l;++i){
        s[i]='9';
    }
    return atoi(s);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值