贪心算法C++详解(知识点+相关LeetCode题目)

目录

一、贪心算法的定义

二、贪心算法LeetCode题目

1.分发饼干 455

2.摆动序列 376

3.最大子数组和 53

4.买卖股票的最佳时机二  122

5.跳跃游戏 55

6.跳跃游戏2  45

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

8.加油站 134

9.分发糖果 135

10.柠檬水找零

11.根据身高重建队列 406

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

13.无重叠区间 435

14.划分字母区间 763

15.合并区间 56

16.单调递增的数字 738

17.监控二叉树 968


一、贪心算法的定义

“贪心算法是一种求解问题时,总是做出在当前看来是最好的选择,不从整体最优上加以考虑的算法。其基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。贪心算法的关键在于贪心策略的选择,而不是对所有问题都能得到整体最优解。

简而言之,就是根据每个阶段的局部最优去推导全局最优

在接下来的题目中,就会发现都是在局部进行地最优的情况下,从而得到最终最优解的

二、贪心算法LeetCode题目

1.分发饼干 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。
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--;//向着逐渐变小的方向
                 result++;//结果递加
             } 
        }

        return result;
    }
};

题解:

满足尽可能多的孩子,在这里使用局部最优的思路,用大饼干满足大胃口的孩子,满足了之后的趋势是饼干不断变小,同时孩子的胃口也在变小,由此尽可能达到全局最优的效果。在过程中计数,最终输出即可


2.摆动序列 376

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

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

  • 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

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

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size() <= 1){
            return nums.size();//返回0或1,否则会重合
        }

        int result = 1;//判断摆动中考虑不到最右边的一个元素
        int preNum = 0;
        int curNum = 0;
        for(int i = 0;i<nums.size()-1;i++){
            curNum = nums[i+1]-nums[i];//默认一开始就是摆动,只要不是0开头
            if(preNum <= 0 && curNum > 0 || preNum >= 0 && curNum < 0){
                //一上一下形成摆动 并且包括了平坡
                result++;
                preNum = curNum;//如果彼此同号,就不必赋值
            }
        }

        return result;
    }
};

题解:

首先要充分理解摆动序列的含义,指的是元素左边的差值与元素右边的差值始终为异号。其次需要理解摆动序列的长度,是需要峰值数+1才是最终的长度。同时要考虑到存在元素相同的情况,这里只需要在判断异号的时候,加上一个等于0,代表左边差值为0的时候,也是一个解。由此来设计整体的逻辑,计数值默认为1,在判断的时候,初始一般为一个峰值。左边的差值由右边决定,因为如果是同号就不会将其计入摆动序列中,只有当出现右边差值与左边异号的情况,才会计入摆动序列,并且继续赋值。


3.最大子数组和 53

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

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
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;
    }
};

题解:

本题采用局部最优的思路,体现在数组的元素不断按顺序向后相加的时候,时刻判断当前的总和是不是历史相加值最大的值,是则存入,不是的话继续判断是否小于0,如果小于0就直接清零,重新向后相加,以此达到局部最优的效果。像这样不断向后累加,就会达到整体最优的效果,从而获得最大和


4.买卖股票的最佳时机二  122
 

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

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

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

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for(int i = 0;i<prices.size()-1;i++){
            result += max(prices[i+1]-prices[i],0);
        }
        return result;
    }
};

题解:

本题采用局部最优的方法,在遍历天数的过程中,时刻确认下一天和当天之间的利润,并且保证相加的时候,利润不可小于0,。其实本题可以抽象把价格数组,直接变成利润数组,转化为数组和这样的题目。因为只要保证当天存在利润,那么往后累加就一定是最大的利润


5.跳跃游戏 55

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

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标
class Solution {
public:
    bool canJump(vector<int>& nums) {
        if(nums.size() == 1){
            return true;//一格则说明已经走到
        }

        int cover = 0;
        for(int i = 0;i<=cover;i++){
            cover = max(i+nums[i],cover);//i代表当前走过几步,cover是可行走的步数,在局部保证可以走的最多
            if(cover >= nums.size()-1){
                return true;//足够到达终点
            }
        }

        return false;
    }
};

题解:

本题是确定是否可以达到最后的坐标,使用局部最优的思想,在行走的过程中依次遍历每一个位置,判断当前位置上可行走的最远距离加上已经行走的距离和之前存储的最远距离哪个大,大的就作为新的最远距离,继续行走(相当于从这里继续向下跳),并且判断当前最远距离是否可以到达终点,如果可以就返回真。如果在行走到最远距离之前都无法成立,就说明无法到达


6.跳跃游戏2  45

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i] 
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。  从下标为 0 跳到下标为 1 的位置,跳 1
 步,然后跳 3
 步到达数组的最后一个位置。
class Solution {
public:
    int jump(vector<int>& nums) {
        if(nums.size() == 1){
            return 0;
        }
        int result = 0;//计数器
        int cover = 0;//当前最远距
        int cur = 0;//之前需要走的距离
        for(int i=0;i<nums.size();i++){
            cover = max(i+nums[i],cover);//计算从当前最远可至的覆盖距离
            if(i == cur){
                //到了地方
                if(i!= nums.size()-1){
                    cur = cover;
                    result++;//起跳
                    if(cur >= nums.size() - 1){
                        break;//已经可以跳过去了(最大长度)
                    }
                }else{
                    break;//已经到达终点
                }
            }
        }

        return result;
    }
};

题解:

本题需要判断最小跳跃次数,但其实核心思想和跳跃次数这道题目相似。在遍历到跳跃位置之前,每次判断当前可以到的最远距离(判断当前位置可行走的最远距离加上已经行走的距离和之前存储的最远距离哪个大),如果遍历到了跳跃的最远距离,位置离终点存在距离(若不存在距离,甚至超过则break),则计数跳跃次数(这里在实际情况中,是从之前遍历过程中可获得到最远距离的地方起跳),值得注意的是,如果之前遍历过程中可获得到最远距离已经大于等于终点距离了,就不必再进行遍历,break即可,这也是直接在这里限制条件了


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

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。

重复这个过程恰好 k 次。可以多次选择同一个下标 i 。

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

示例 1:

输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。
class Solution {
private:
    static bool cmp(int a,int b){
        return abs(a) > abs(b);//将两数中较大的优先选择(排列在前面)
    }
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        int maxNum = 0;
        sort(nums.begin(),nums.end(),cmp);
        for(int i = 0 ;i<nums.size();i++){
            if(nums[i] <0 && k >0){
                k--;
                nums[i] *= -1;//在k的次数内,尽可能把大的负数变为正数
            }
        }

        if(k>0){
            if(k%2 == 1){
                //如果k是偶数,实际上乘完k次后,相当于没有乘,节省性能只需要计算奇数即可
                nums[nums.size()-1] *= -1;
            }
        }

        for(int a : nums){
            maxNum += a;
        }

        return maxNum;
    }
};

题解:

本题需要计算在k次取反后,可以返回的最大值。在这里使用的思路是基于局部最优的,即将数组按照绝对值进行降序排序,前面最小的进行取反,只会对最终的和有利而无弊,因为前面的如果是负数,就会变成正数,增加最终的和,如果前面的为正数,那就限制不可以用来消耗掉k的次数。如果在遍历完之后,k有剩余,并且k为奇数(累乘后仍为-1),则将数组最后一个元素乘上-1,因为最后一个元素的绝对值最小,用它来消耗k最优


8.加油站 134

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

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

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

示例 1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curmin = 0;//当前油量的最小值
        int min = INT_MAX;//油量最小值
        for(int i =0;i<gas.size();i++){
            int leftgas = gas[i]-cost[i];//当次还剩余的油量
            curmin += leftgas;
            if(curmin < min){
                min = curmin;//得到最小的油量剩余总和
            }
        }

        if(curmin <0){
            return -1;//如果将全部差值累加后的curmin都小于0了,那么一定不存在解了
        }
        if(min >= 0){
            return 0;//说明每一块都会满足有油可用,则开车从零开始即可
        }

        //最小值小于0的时候,则需要找到出发节点出发后,可以将总和最小值填平的解,所以后序遍历
        for(int i = gas.size()-1;i>= 0;i--){
            int leftgas = gas[i]-cost[i];
            min+= leftgas;
            if(min>=0){
                return i;
            } 
        }

        return -1;//即找不到解可以将min抵消
    }
};

题解:

本题采用的思路是先计算至当前站的剩余油量(加过油之后),以此去得到剩余油量中的最小值。接着进行判断,如果至最后一站的剩余油量小于0,那么说明不可能存在解,如果剩余油量中的最小值大于0,那么说明从0处出发即可,不用担心油的损耗。接下来就要对一般情况进行讨论,即至最后一站的剩余油量是大于0的,但是剩余油量中的最小值小于0,所以需要后序遍历,最小值加上每次剩余的油量,找到一处地方,使得最小值大于0,那么从那里出发之后,就可以把最小值给抵消掉,完成开一圈的任务


9.分发糖果 135

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

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

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

示例 1:

输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candy(ratings.size(),1);//变为大小相同数值都为1的数组
        //先从左边向右边,如果右边大,则加一,否则不变
        for(int i =1;i<candy.size();i++){
            if(ratings[i]>ratings[i-1]){
                candy[i] = candy[i-1]+1;//只会影响到当前的大小,如果不连续,下一个还是从1开始
            }
        }

        //再从右边到左边,如果左边大,则加一,否则不变
        for(int i = candy.size()-2;i>=0;i--){
            if(ratings[i]>ratings[i+1]){
                candy[i] = max(candy[i+1]+1,candy[i]);//在满足相邻条件的同时,不可以和左边向右边的结果冲突
            }
        }

        int result =0;
        for(int a :candy){
            result += a;
        }

        return result;
    }
};

题解:

本题需要满足的条件,相邻的两者,分数高的获得糖果要高,因为准备最少的糖果数,所以加一即可。首先以人数大小建立糖果数组。相邻就分成两种情况,是从左向右的相邻比较,还是从右向左的相邻比较。所以分情况,进行遍历讨论,从左至右,如果大于则较大元素的值为较小元素的值加一,再从右至左,这里虽然大体逻辑还是比较大的,然后大的值为小的值加一,但是需要考虑到从左至右的遍历赋值结果,所以在加一后需要和原来的值做比较,取较大的那个,这样可以兼容两种结果。最后遍历赋值过的结果,返回累加结果即可


10.柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

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

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:

输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five =0,ten =0;//定义好每张面额的数量
        for(int i : bills){
            if(i==5){
                five++;
            }else if(i==10){
                if(five == 0){
                    return false;
                }else{
                    ten++;
                    five--;
                }
            }else if(i==20){
                if(five >0 && ten >0){
                    five--;
                    ten--;
                }else if(five >=3){
                    five = five-3;
                }else{
                    return false;
                }
            }
        }

        return true;
    }
};

题解:

本题的思维就是如何去局部最优地处理找零,例如面对10,只有用5,而面对20,显然是10+5找零更好,因为5的兼容性强,既可以找零10又可以找零20,所以在面对每一份账单的时候,需要理解好10和5的使用优先度,然后完成遍历,返回真假。


11.根据身高重建队列 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]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
class Solution {
public:
    static bool cmp(vector<int>& a,vector<int>& b){
        if(a[0]==b[0]){
            //当元素值相同的时候,谁的k大排后面
            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>> result;
        for(int i=0;i<people.size();i++){
            int index = people[i][1];//得到当前的K大小
            result.insert(result.begin()+index,people[i]);//因为k的目的是前面存在k个大于等于的数,所以插入到k个后
        }

        return result;
    }
};

 题解:

本题充分利用了贪心算法局部最优的思想,结合题目的条件,元素前需要有k个大于等于该元素的点,则可以使用插入元素的方法,保证每次插入最优,返回结果即可。那么首先是需要排序,同时保证同样大小的元素,k小的在前面(为了后面的满足插入)。之后就是遍历,这里只需要按顺序插入对应的位置便是局部最优,因为k的大小决定了其插入的位置,而且前面存在的元素,一定都满足了条件(大小排序过了),不用详细数学证明,因为到这为止,已经是局部最优的方案,如果不满足,则说明提供的元素集有问题


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

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

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

给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 

示例 1:

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
class Solution {
public: 
    static bool cmp(vector<int>& a,vector<int>& b){
        return a[0]<b[0];//从小至大排序,并且由左边界决定
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(),points.end(),cmp);
        int result = 1;//至少一箭-最后一个/堆气球一定需要一箭
        for(int i = 1;i<points.size();i++){
            if(points[i][0] > points[i-1][1]){
                result++;//完全不重叠
            }else{
                //当前元素的范围的左边界和前一个元素的右边界重叠,为了下一次判断,如果下一个元素的左边界大于两者最小右边界,则需要射出箭(对这两个),如果小于,则继续min(为了下下次判断)--这就是一箭穿多气球
                points[i][1] = min(points[i-1][1],points[i][1]);
            }
        }
        return result;
    }
};

题解:

这道题实质上是对每个气球的范围是否会重合进行判断,如果重合了只需要射一箭,如果不重合,就需要多射一箭。所以采用左边界大小排序的方式,根据右边界的大小去判断是否重合。在逻辑设计上,因为是和前一个比较边界,所以需要设置射箭数一开始就为1,因为最后的一次射箭会不被考虑到。在重合判断中还需要注意,可能出现多个重叠的情况,只需要每次将重合的范围的右边界取最小的即可(交集),这样往下判断,即可考虑到多个重叠


13.无重叠区间 435

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
class Solution {
public:
    static bool cmp(vector<int>& a,vector<int>& b){
        return a[0]<b[0];//左边界从小至大排序
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(),intervals.end(),cmp);
        int count = 0;//计算需要移除的次数
        for(int i = 1;i<intervals.size();i++){
            if(intervals[i][0] < intervals[i-1][1]){
                //如果当前的节点的前一个节点右边界大于当前节点左边界,则是重合
                count++;
                intervals[i][1] = min(intervals[i-1][1],intervals[i][1]);//“移除”是移除掉右边界稍大的,保留右边界较小的,这样可以局部最优避免重合
            }
        }
        return count;
    }
};

题解:

本题和用最少的箭引爆气球类似,虽然题目中要求的是去除,但实际上也不用真的去除其元素。需要采用局部最优的思想,当然这样的区间问题,首先要确定排序顺序,在这里采用的是左边界的从小到大排序。然后再依次遍历,比较当前节点与前一个节点,如果重合就视为次数加一,同时最关键的是重合要“剔除”哪个元素,为了尽可能少的去除元素,所以局部优先选择右边界较小的保留,也就是将值赋值给当前节点的右边界,以进行下一次比较


14.划分字母区间 763

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int> result;
        int letter[26] = {0};//字母和数量对应数组
        for(int i = 0;i<s.size();i++){
            letter[s[i]-'a'] = i;//最终字母对应的数字就是最远的位置
        }

        int left = 0;
        int right = 0;
        for(int i = 0;i<s.size();i++){
            right = max(right,letter[s[i]-'a']);//为了包括之前的解
            if(right == i){
                //如果已经对上了索引,说明到这里已经包括了前面的所有字母
                result.push_back(right-left+1);//存储长度
                left = right +1;
            }
        }

        return result;
    }
};

题解:

本题需要通过遍历,时刻获取之前字母中的最远位置,一旦当前存储的最远位置和索引相同,就直接说明到这里的字符串已经包括了之前字母的所有出现情况(因为在比较谁的位置更远的时候,已经包括了结束位置较前的子母)。字母的最远位置通过遍历赋值即可获得


15.合并区间 56

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例 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:
    static bool cmp(vector<int>& a,vector<int>& b){
        return a[0]<b[0];//从小至大排序
    }
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;//结果数组
        sort(intervals.begin(),intervals.end(),cmp);
        result.push_back(intervals[0]);//先存入第一个数组
        for(int i =0;i<intervals.size();i++){
            if(result.back()[1] >= intervals[i][0]){
                //重叠区间
                result.back()[1] = max(intervals[i][1],result.back()[1]);//获取最大值,作为新的边界,刷新区间范围
            }else{
                //区间不重叠
                result.push_back(intervals[i]);//重新存入新的区间
            }
        }

        return result;
    }
};

题解:

本题仍然为区间问题,在判断重叠区间上,逻辑和一般区间问题一致,也是以区间的左边界按顺序排序,然后逐一比较左边界与右边界,区别在于,输出区间的解。为了输出重叠区间的范围,那么需要满足重叠条件的时候,刷新结果数组的尾元素的右边界(比较谁的右边界更大)。一旦不满足条件的时候,从后面存入新的区间,之后按照逻辑不断刷新该区间(此时已经是结果数组的尾元素)的右边界,遍历目标数组,最终得到结果数组。


16.单调递增的数字 738

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

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

示例 1:

输入: n = 10
输出: 9
class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string Num = to_string(n);
        int flag = Num.size();//默认不占位置
        for(int i = Num.size()-1;i>0;i--){
            if(Num[i-1] > Num[i]){
                //高位大于低位
                Num[i-1]--;//高位减少值
                flag = i;//保存低位的位置
            }
        }

        for(int i = flag;i<Num.size();i++){
            //低位的最前位置开始,全部变成9
            Num[i] = '9';
        }

        return stoi(Num);
    }
};

题解:

本题采用的思路是倒序寻找不满足递增的数字,并且将位置最前的数字位置保存下来,通过这一位置确定后面的数字为非递增并且位数较低,那么则采用局部最优的思路,遍历将包括该位置之后的低位数字全部化为‘9’,如此便一定满足数字的递增且最大的要求


17.监控二叉树 968

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
class Solution {
private:
    int result = 0;
    int GetResult(TreeNode* root){
        //终止条件
        if(root == nullptr){
            //空节点默认为已覆盖2(为了让叶子结点的父节点为摄像头位置)
            return 2;
        }
        int left = GetResult(root->left);
        int right = GetResult(root->right);

        //分类讨论
        

        if(left == 2 && right == 2){
            //子节点确定都已经覆盖 22
            return 0;
        }

        if(left == 0 || right == 0){
            //子节点都为未覆盖的 或者存在未覆盖的 00 01 02 
            result++;
            return 1;
        }

        if(left == 1 || right ==1 ){
            //子节点确定都为摄像头 或者一个摄像头一个已覆盖 11 12 --以上已经把其他解概括了
            return 2;
        }

        return -1;//假返回,上面已经包括了所有的解
    }
public:
    int minCameraCover(TreeNode* root) {
        //未覆盖0
        //摄像头1
        //已覆盖2
        if(GetResult(root) == 0){
            //如果最终根节点是未覆盖,则自身安装摄像头
            result++;
        }
        return result;
    }
};

 题解:

本题的基础思想仍然是贪心算法,就是怎么安置摄像头是局部最优的,通过分类讨论,可以发现只有当摄像头放置在叶子节点的父节点上才是最好的方式,那么接下来就围绕其展开算法。为了计算方便,可以将未被覆盖设为0,安装摄像头为1,已被覆盖设置为2。因为要对子节点进行分类讨论,所以采用后序遍历的方法对树。分为三大种情况,为了书写方便,先把容易讨论的,放在if判断句的前面,首先就是子类存在未被覆盖的,那就一定要把该节点安装摄像头(1),其次是子类都已经被覆盖,那就把该节点设置为未覆盖(0),让该节点的父节点再安装,这样最优,接下来就是子节点要么安装了摄像头,要么已经被覆盖,这些情况都统一需要该节点设置为已覆盖(2),因为还存在子节点都为摄像机的情况,所以设置该节点为已覆盖为更优解。最终记得讨论特殊情况,即根节点为未覆盖,那么就需要其自身安装摄像头。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

花火の云

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值