二刷力扣——贪心算法

贪心算法其实就是没有什么规律可言,所以大家了解贪心算法 就了解它没有规律的本质就够了。

贪心本质是选择每一阶段的局部最优,从而达到全局最优。如果每步最优解不能保证到达全局最优,就不能用。

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

解决优化问题。

455. 分发饼干

饼干不能掰开。

解决方法:得先满足胃口小的孩子,所以得排序;要用双指针的办法的话,指针一直往前走,g不排序不行,比如:g=3,2,1;s=1,2,3。

所以都排序,遇到可满足的就count++,i++;j++。遇到不可满足的就看下一块饼干:j++。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        int i=0,j=0,count=0;
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        for(int j=0;j<s.size()&&i<g.size();++j){
            if(s[j]>=g[i]){
                count++;
                i++;
            }
        }
        return count;
    }
};


376. 摆动序列

认为边上2个一定是 是错的,比如[0,0,0]

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size()==1)return 1;
        if(nums.size()==2 &&nums[0]==nums[1])return 1;
        int count=2;//边上2个一定是
        //统计有几个极值
        for(int i=1;i<nums.size()-1;++i){
            int l=nums[i]-nums[i-1];
            int r=nums[i]-nums[i+1];
            if(l*r>0)count++;;
            
        }
        return count;
    }
};

可以发现:

没解决好:元素相等的情况——wrong.

总的:不要先统计两边的,先统计前两个因为pre计算前2个的。

pre是上一个升降,cur是下一个。

只会统计升降,也就是cur大于小于的情况,等于的话是跳过的。所以:极值数==升降数+1(这个1开头就加了, int count=pre==0?1:2;这里),后面就统计剩下的升降数。找规律:从[2,】开始,两相邻数之差不为0就是一个升/降,这样就可以统计+1了吗?不行,如果pre(前一个趋势和cur是一样的,只能算是一个升降,pre是已经统计过的,所以得跳过),只有当pre和cur的趋势不一致(pre>0且cur<0||pre<0且cur>0)才能+1;而且初始的pre还有可能为0(之后的pre不可能是0),pre为0 的时候如果cur是升降,也需要统计+1,所以总的条件就是下面的if。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size()==1)return 1;
        int pre=nums[1]-nums[0];//pre和cur指向的区间趋势相反
        int count=pre==0?1:2;//不相等为什么可以是2——因为把第二个替换成极值
        //从[2,统计有几个极值——几个升降
        for(int i=2;i<nums.size();++i){
            int cur=nums[i]-nums[i-1];
            if((cur>0 && pre<=0)||(cur<0 && pre>=0)){//发现一个和pre相对的升降
                pre=cur;
                count++;
            }
        }
        return count;
    }
};

所以,不光是发现升降,要发现一个和pre相对的升降。

53. 最大子数组和

还记得一刷的一点印象,sum一直加上元素,如果sum<0了直接sum重置为0。还要设一个maxSum,统计循环里面sum的最大值,注意取最大值要在sum重置为0的前面,因为结果也可以是负数的。

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

时复O(n);空复O(1)

是专注于自己这个元素,如果前面加上自己比自己还小,那就干脆不加前面的(也就是sum重置)。

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

还记得,跟上面的有点像。我站在上帝视觉,知道明天和今天的股票,所以每天都会试试水,尝试今天买入和明天卖出,如果赚了(chajia>0),就这样做。如果不赚,就不做。

所以count只会加上相邻2天是可以赚到(>0)的差价,就只会往越来越多的趋势走。体现贪心思想:局部最优推全局最优

代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int count=0;
        for(int i=1;i<prices.size();++i){
            int chajia=prices[i]-prices[i-1];
            if(chajia<0)continue;
            else count+=chajia;
        }
        return count;
    }
};

就下面的思想:

 55. 跳跃游戏

因为只要知道能不能跳到最后,所以看最大覆盖范围能不能到最后就行。

1、最后的right(覆盖范围)≥最后的位置就可以返回true。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int right=nums[0];//覆盖范围
        for(int i=0;i<=right;++i){//挨着跳
            right=max(right,i+nums[i]);
            if(right>=nums.size()-1)return true;
        }
        return false;
    }
};

2、

class Solution {

public:

    bool canJump(vector<int>& nums) {

        int k = 0;

        for (int i = 0; i < nums.size(); i++) {

            if (i > k) return false;

            k = max(k, i + nums[i]);

        }

        return true;

    }

};

作者:Ikaruga
链接:https://leetcode.cn/problems/jump-game/solutions/24322/55-by-ikaruga/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

45. 跳跃游戏 II

要得到最小跳跃次数。怎么从上面的基础上来做。

次数最小就是要求一次尽量跨最大步。

1、所以一种反向的思路是: i 从0开始,往后看(i++)有没有坐标i能走到right位置,如果可以,第一个遇到的坐标i和right肯定是距离最大,这个时候可以count++,然后更新right值(目标位置),而且又要重复这个过程,即把i重置为0。

class Solution {
public:
    int jump(vector<int>& nums) {
        int count=0,right=nums.size()-1;
        for(int i=0;i<right;){
            if(nums[i]>=right-i){
                right=i;
                count++;
                i=0;
            }
            else {
                i++;
            }
        }
        return count;
    }
};

但是有点慢,因为如果是[1,1,1,1,1,1]这样的输入,时间复杂度退化到O(n^2),所以想一下一趟线性时间就能搞定的方法。

2、正向:

和上面一样,记录能到达的最大边界right。然后i在这个边界里面(0到right)找下一步能走的最大步,

但是什么时候count++?right更新下一步还不能加。

要在设置一个变量cur,代表这一步能到达的边界,当走到这个边界(i==cur)的时候,就是走了最大的一步,这个时候count++。然后走下一步,cur则更新为maxPos,所以maxPos记录的是下一步能到达的边界。

如果cur更新之后,边界值超过了nums边界,就可以返回了。

class Solution {
public:
    int jump(vector<int>& nums) {
        if(nums.size()==1)return 0;
        int count=0;
        int maxPos=0;//下一步可到达的边界
        int cur=0;  //这一步可到达的边界
        for(int i=0;i<nums.size();++i){
            maxPos=max(maxPos,i+nums[i]);//步子尽量跨大
            if(i==cur){
                count++;
                cur=maxPos;
                if(cur>=nums.size()-1)return count;
            }
        }
        return count;
    }
};




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

应该先按照绝对值从大到小排,然后遍历每个元素,把遇到的≤k个的负数都取反再加:非负数正常加。然后如果负数都取反了k还不为0而且是奇数,就把绝对值最小的那个非负数取反再加(因为可以可以多次选择同一个下标)。这样保证前面取反的是绝对值较大的一些负数,加起来最大,最后取反的是绝对值最小的那个非负数,减小的最少。综合是最大值。

class Solution {
public:
    static bool cmp(int a,int b){
        return (abs(a)>abs(b));
    }
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        int sum=0,i=0;
        sort(nums.begin(),nums.end(),cmp);
        for(int i=0;i<nums.size()-1;++i){
            if(k>0 && nums[i]<0){
                sum-=nums[i];
                k--;
            }
            else{
                sum+=nums[i];
            }
        }
        if(k>0&&k%2==1)sum-=nums.back();
        else sum+=nums.back();
        return sum;
    }
};

力扣官方使用了map来做,再看看:

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        unordered_map<int, int> freq;
        for (int num: nums) {
            freq[num] += 1;
        }
        int ans = accumulate(nums.begin(), nums.end(), 0);
        for (int i = -100; i < 0; ++i) {
            if (freq[i]) {
                int ops = min(k, freq[i]);
                ans += (-i) * ops * 2;
                freq[i] -= ops;
                freq[-i] += ops;
                k -= ops;
                if (k == 0) {
                    break;
                }
            }
        }
        if (k > 0 && k % 2 == 1 && !freq[0]) {
            for (int i = 1; i <= 100; ++i) {
                if (freq[i]) {
                    ans -= i * 2;
                    break;
                }
            }
        }
        return ans;
    }
};

作者:力扣官方题解
链接:https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/solutions/1134002/k-ci-qu-fan-hou-zui-da-hua-de-shu-zu-he-4r5lb/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

134. 加油站

用一次遍历来做,如果穷举所有位置然后走一圈,会超时。

优化的办法就是:

即:因为x可以到y+1前面的位置,剩余油量总是>0,但是到y+1就没有油了,说明这一段路把之前到y剩下来的油都能用完,那么肯定,从(x,y]之间任选一个点,走到了y的时候剩余油量比之前x开始的时候还少,再走[y,y+1]这段路肯定更不够。所以只能从y+1开始了。

找这个起点的循环,只用 从头到尾 顺序找就行了,因为假如找到一个start,能跑到结尾,但是不能跑一圈,当且仅当(maybe吧)总的汽油值是小于总的cost值的。所以用一个totalSum记录从0到结尾的 汽油总值减去cost总值,如果不够返回-1。如果>=0就肯定最后找到的start就是正确答案。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int totalSum=0,curSum=0,start=0;
        for(int i=0;i<gas.size();++i){//从左到右,不用掉头
            totalSum+=(gas[i]-cost[i]);//totalSum的作用
            curSum+=(gas[i]-cost[i]);//起点start到结尾的剩余
            if(curSum<0){//直接下一个开始
                start=i+1;
                curSum=0;
            }
        }
        if(totalSum<0)return -1;
        return start;
    }
};


力扣官方题解:从start开始,往后从0累加curSum,<0的话会break;经过的站==n了也会退出内循环。

所以内循环外面判断如果是经过了n站了说明能走一圈所以直接返回start起点;否则就是break导致出循环,那么根据上面的原理更新start为下一个位置。

这里有走一圈的循环(i),没有用上面totalSum的原理。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int start=0;
        int n=gas.size();
        while(start<n){    //找起点
            int zhan=0;       //zhan:经过了几站
            int curSum=0;
            while(zhan<n){
                int i=(start+zhan)%gas.size();//遍历[start,]的
                curSum+=(gas[i]-cost[i]);
                if(curSum<0)break;
                zhan++;
            }
            if(zhan==n){//找到,可以走一圈
                return start;
            }
            else //从start走不到start+zhan。
            {
                start=start+zhan+1;
            }
        }
        return -1;
    }
};


135. 分发糖果

一刷的时候采用的办法:分两遍处理,先从左往右处理<的。再从右往左处理>的。

处理操作是:如果自己比 左边/右边 大 但是分到的糖果不是更大(if)就更新为更大(更新数组元素),要求 最少糖果数 所以比 左边/右边 大 一个就行。

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> result= vector<int>(ratings.size(),1);
        for(int i=1;i<ratings.size();++i)
        {
            if(ratings[i]>ratings[i-1]&&result[i]<=result[i-1])result[i]=result[i-1]+1;
        }
        for(int i=ratings.size()-2;i>=0;--i)
        {
            if(ratings[i]>ratings[i+1]&&result[i]<=result[i+1])result[i]=result[i+1]+1;
        }
        int sum=0;
        for(auto a:result){
            cout<<a<<endl;
            sum+=a;
        }
        return sum;
    }
};

但是时间开销大,遍历了两遍。而且空间复杂度O(n)。

这也是力扣官方题解方法一:两次遍历

但是为啥这样是最少的?不清楚

法2:

对上图修改一下:

这个5就要并入递减序列里面算才对。这说明了一种特殊情况,红框的话:

总的:参考评论

这里pre代表i 及 i 前面,递增序列的最后的数。所以遇到递减序列,pre就得重置为1.然后递减序列长度dec++;如果发现==inc了,对应特殊情况,所以dec还得+1,这样把递增递减中间的峰值划到递减序列;最后就给目前的和之前 的递减序列里面的都+1(效果等同+dec本身)。

遇到非递减序列,dec得重置为0.pre是什么?目前递增序列的最后的数,所以更新pre。把>=2个情况写一起的。然后结果加上这个pre——i位置的糖果数(加inc也可以,因为都从1开始加)。最后inc更新——不是直接++,而是等于pre——因为递增和等于的情况一起写的。

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        int ret = 1;//最少糖果数目 
        int inc = 1, dec = 0, pre = 1;
        for (int i = 1; i < n; i++) {
            if (ratings[i] >= ratings[i - 1]) {
                dec = 0;
                pre = ratings[i] == ratings[i - 1] ? 1 : pre + 1;//更新i位置的糖果数
                ret += pre;
                inc = pre;
            } else {
                dec++;
                if (dec == inc) {//要把最近的递增序列的最后一个同学也并进递减序列中。递减序列++
                    dec++;
                }
                ret += dec;//为什么+dec?
                pre = 1;
            }
        }
        return ret;
    }
};

为了更好的理解,尝试把>=分开并且inc用++来实现,但是有的例子不能通过。


class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        int ret=1;//0下标糖果
        int dec=0,inc=1,pre=1;//第一个算递增里面
        for(int i=1;i<ratings.size();++i){
            if(ratings[i]>ratings[i-1])//≥放一起写
            {
                pre++;//用pre更新i的糖果数
                inc++;
                dec=0;//不再连续递减
                ret+=pre;
            }
            else if(ratings[i]<ratings[i-1])
            {
                dec++;
                if(dec==inc){
                    dec++;
                }
                ret+=dec;
                pre=1;
            }
            else
            {
                pre=1;//用pre更新i的糖果数
                inc=1;
                dec=0;//不再连续递减
                ret+=pre;
            }
        }
        return ret;
    }
};


860. 柠檬水找零

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five=0,s=0;
        for(int i=0;i<bills.size();++i){
            if(bills[i]==5){
                five++;
            }
            else if(bills[i]==10){
                if(five>0){
                    five--;
                    s++;
                }
                else return false;
            }
            else{//20
                if(s>0&&five>0){
                    s--;
                    five--;
                }
                else if(five>2){
                    five-=3;
                }
                else return false;
            }
        }
        return true;
    }
};

浏览评论找到代码可以直接把上面过程浓缩成一行:?:语法代替if-else结构:

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        return all_of(bills.cbegin(), bills.cend(), [a = 0, b = 0](int i)mutable { return i==5?++a:i==10 ? (++b,a--) : b ? (b-- && a--) :((a -= 3)>-1); });
    }
};

all-of函数里面用Lambda表达式。这行代码前++后++好像不能随便定,否则出错。



406. 根据身高重建队列

和分发糖果类似,不要两头兼顾,处理好一边再处理另一边。

前面刚好有ki个,那先进行排序,希望得到排序完之后的people是:序列里面的每个人的前面所有人都比自己高,或者相等,相等的话前面那个序号较小,这样的话从左到右,每个元素直接插入到位置是ki的地方,就能保证前ki个都是比自己高的了。

要保证最后结果这个身高得是从大到小的,所以当身高相等的时候,序号得从小到大,身高不相等的话就从大到小排好。

cmp实现:

static bool cmp1(vector<int> vec1,vector<int> vec2){
        if(vec1[0]==vec2[0])return vec1[1]<vec2[1];
        return vec1[0] > vec2[0];
    }

如果先插入i处元素,再删除,最后一个元素不会是对的。

for(int i=0;i<people.size();++i){
            
            people.insert(people.begin()+people[i][1],vec);//insert和erase位置都得是迭代器
            people.erase(people.begin()+i);
        }

其实原地修改操作不太方便,先插入再删除还是反过来都是。

所以还是用一个辅助空间,辅助空间也定义为 vector<vector<int>> 吗。vector底层是普通数组实现的。vector的容量capacity预先设定为1,size到了capacity之后,这个capacity就会增加到原先的两倍(或者其他倍),所以用vector做插入效率不高。增删时间复杂度低的数据结构是链表,所以用链表来访结果进行好一些。

list容器放正确顺序的结果。注意链表不能随机访问,所以插入的时候要确定迭代器的位置,而不能用插入位置不能用begin()+下标。

class Solution {
public:
    static bool cmp1(vector<int> vec1,vector<int> vec2){
        if(vec1[0]==vec2[0])return vec1[1]<vec2[1];
        return vec1[0] > vec2[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
     
        sort(people.begin(),people.end(),cmp1);
        
        list<vector<int>> result;
        for(int i=0;i<people.size();++i){
            list<vector<int>>::iterator it=result.begin();
            int pos=people[i][1];
            while(pos--){
                it++;//确定插入位置
            }

            result.insert(it,people[i]);//插入
        }
        return vector<vector<int>>(result.begin(),result.end());
    }
};

时间:排序O(nlogn),插入O(n^2)。总的O(n^2)。

空间复杂度O(n)。

当然也可以用vector放结果,更简洁:

class Solution {
public:
    static bool cmp1(const vector<int>& vec1,const vector<int> &vec2){
        if(vec1[0]==vec2[0])return vec1[1]<vec2[1];
        return vec1[0] > vec2[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
     
        sort(people.begin(),people.end(),cmp1);
        
        vector<vector<int>> result;
        for(const vector<int>& person  :people){
            result.insert(result.begin()+person[1],person);//插入到result的person[1]下标处
        }
        return result;//vector<vector<int>>(result.begin(),result.end());
    }
};

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

参数和上面一样的结构,感觉首先需要排序。

第一个元素从小到大排比较直观(习惯从左到右)。

所以遍历的cur气球就会有上面3种情况:左边界小于 上一个射出的弓箭 可覆盖的右边姐①②;大于……③。如果是①②。上一个弓箭还能接着用,但是弓箭射出位置在情况①得变化,总的就是选择上面右边界和cur右边界的最小值;如果是③,就得使用新的弓箭,但是弓箭射出位置应该选左边还是右边呢?应该是右边,右边才能尽可能多的覆盖其他没有遍历到的气球。

所以弓箭数count一旦增1,射出位置就是:(这里原本引爆的气球就是新射出的弓箭可以因报的气球)注意红框。

class Solution {
public:
    static bool cmp(const vector<int> &v1,const vector<int> &v2)
    {
        return v1[0]<v2[0];
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(),points.end(),cmp);
        int count=0;
        int pre=INT_MIN;//上只箭射出的位置
        for(const vector<int>& point:points)
        {
            if(point[0]>pre ){//必须再用1只箭
                count++;
                pre=point[1];
            }
            else pre=min(pre,point[1]);
        }
        return count==0?1:count;//有一个例子比INT_MIN小所以这样写
    }
};

最后的return 这样写是为了:

时间复杂度:O(nlogn)。排序决定

空间:O(logn)。排序决定。

和下面几题都是重叠区间 问题

注意到如果排序第二个元素(右边界的话),下一个区间右边界肯定比pre大,所以只有②③两种情况,②的话不需要新箭,射出位置本来就是要选最小的,原本的就是最小的,所以不用更新;③需要新箭,射出count++,然后更新射出位置。

class Solution {
public:
    static bool cmp(const vector<int> &v1,const vector<int> &v2)
    {
        return v1[1]<v2[1];
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(),points.end(),cmp);
        int count=0;
        int pre=INT_MIN;//上只箭射出的位置
        for(const vector<int>& point:points)
        {
            if(point[0]>pre ){//必须再用1只箭
                count++;
                pre=point[1];
            }
        }
        return count==0?1:count;
    }
};

435. 无重叠区间

可以直接用上题的方法来做,要 求 移除区间 的 个数,可以间接求不重复区间(剩余区间)有几个,总数减去不重复区间 ,结果就是 移除区间 的个数。

剩余区间 之间的边界可以重合,所以和上面不一样的是,元素左边界 等于curRight的时候也得算做一个:

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(),intervals.end(),[](const vector<int>& v1,const vector<int>& v2){return v1[0]<v2[0];});
        int count=1;
        int curRight=intervals[0][1];
        for(int i=1;i<intervals.size();++i)
        {
            if(intervals[i][0]>=curRight){
                count++;
                curRight=intervals[i][1];
            }
            else curRight=min(curRight,intervals[i][1]);
        }
        return intervals.size()- count;
    }
};

和上题同理排序右边界的话不用else条件:


class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(),intervals.end(),[](const vector<int>& v1,const vector<int>& v2){return v1[1]<v2[1];});
        int count=1;
        int curRight=intervals[0][1];
        for(int i=1;i<intervals.size();++i)
        {
            if(intervals[i][0]>=curRight){//计算新的重复区间
                count++;
                curRight=intervals[i][1];
            }
        }
        return intervals.size()- count;
    }
};

和上面的是一个道理。要选择的位置是:原本引爆气球(这里是多个重复区间中)中最靠左的右边界位置。

763. 划分字母区间

需要知道每个字母的最后一次出现的下标位置,可以用map,也可以用数组来映射(因为s 仅由小写英文字母组成)

也是要不断更新右边界:

上述做法使用贪心的思想寻找每个片段可能的最小结束下标,因此可以保证每个片段的长度一定是符合要求的最短长度:要是更短的话,同一个字母会出现在不同序列里面。

下面是不用辅助空间实现的,但是很慢。因为找最少结束下标瑶遍历来找(j从后往前),时间复杂度O(n^2)

class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int>  result;
        int start=0,maxRight=INT_MIN;
        for(int i=0;i<s.size();++i)//字母最后出现的位置
        {
            int j=s.size()-1;
            for(;j>=i;j--)
            {
                if(s[j]==s[i])break;
            }
            maxRight=max(maxRight,j);
            if(maxRight==i)
            {
                result.push_back(i-start+1);
                start=i+1;
            }
        }
        return result;
    }
};

所以用辅助空间,数组实现,两次遍历:第一次遍历不断更新获得每个字母的最后位置;第二次遍历检查下标end值,找到则添加到result……时间复杂度O(n)

class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int>  result;
        int start=0,end=INT_MIN;
        int last[26];
        for(int i=0;i<s.size();++i)//字母最后出现的位置
        {
            last[s[i]-'a']=i;//最后更新的就是最后下标位置
        }
        for(int i=0;i<s.size();++i)
        {
            end=max(end,last[s[i]-'a']);
            if(end==i)//当前片段访问结束
            {
                result.push_back(i-start+1);
                start=i+1;
            }
        }
        return result;
    }
};

用int数组做银蛇比map更省空间和时间,因为这里key值就26个,而且key值(a-z)可以映射到0-25(s[i]-'a'),所以 再以这个值 作为下标,再映射到下标 i 去。

56. 合并区间

仍然和射箭/无重叠区间 是一类题:先排好序(这里排v1[0],所以当新的区间出现直接start赋值下一个的左边界intervals[i][0]就好)。

[start,end]记录一个合区间,当下一个区间的左边界脱离这个合区间了(>end),就需要把旧的合区间收集记录下来;否则需要更新end值(v1[1]没有排;得取较大值来延伸这个合区间)。

最后还要收集一下最后的合区间:

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;
        sort(intervals.begin(),intervals.end(),[](const vector<int>& v1,const vector<int>& v2){return v1[0]<v2[0];});
        int start=intervals[0][0];
        int end=intervals[0][1];
        for(int i=1;i<intervals.size();++i)
        {
            if(intervals[i][0]>end)//新的区间
            {
                result.push_back({start,end});//收集旧的区间
                //更新为新的区间
                start=intervals[i][0];
                end=intervals[i][1];
            }
            else end=max(intervals[i][1],end);
        }
        result.push_back({start,end});
        return result;
    }
};

和上面不一样的就是这里end记录最右边界得取最大值(并的思想,这个最大值是一个合区间能到的最远处),而上面两道题是取最小值(交的思想,这个最小值涵盖一个count代表的所有区间,即一个count代表的所有区间的右边界的最小值)

738. 单调递增的数字

是两位数的话,假设ab:a>b,那么符合条件的数就是(a-1)9,即把个位变成9,十位变成1。到一般情况:n位数,x1 x2 …… xn。如果xi > xi+1,仅仅把后一位xi+1变成9,前一位xi-1——那么xi+1到xn不会满足单调递增。所以只要发现有一对是单调递减的,就要记录下来。每次找到一个开始下降的一对,前面的-1后面的置9,数字又变化,所以还得往前检查,直到左边界。即在找到了最前面的单调递减的一对之后,这一对的第二个元素以及之后的都要变成9才符合条件。

总的思想就是:

比如:1、332。32:2改9,3-1,变成29。总的变成329。再看前面的32,同样改成29,所以是229。

2、101。01符合,10不符合,改成099。

注意设置为9的位置变量是pos,初始化为s.size()。比如1234就任何位都不会设置为9.

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string s=to_string(n);
        cout<<s<<endl;
        int pos=s.size();
        for(int i=s.size()-1;i>=1;--i)
        {
            if(s[i-1]>s[i])
            {
                pos=i;//不断更新找到最前面的递减对
                s[i-1]--;
            }
        }
        for(int i=pos;i<s.size();++i)s[i]='9';
        return stoi(s);
    }
};

复杂度是n的位数不是n的值。

968. 监控二叉树

要求最小摄像头数量。

用递归怎么做?

摄像头可以覆盖上中下三层,所以不放在树的叶节点上面

所以1、选择后序遍历。从叶节点往上找最小数(贪心体现在此)

2、要统一设置几种状态。递归函数要 根据孩子的情况(递归返回的状态数字)推父节点的情况然后决定要不要设置摄像头,设置几种状态呢?所有节点就3种情况:0有摄像头、1没摄像头但被覆盖、2没被覆盖。

所以2个孩子的情况就有6种,不区分左右了,对称的。返回的是root应该的值:

1、左0右0。root得是1

2、左0右1。root得是1

3、左0右2。root得是0,所以摄像头数++。

4、左1右1。root得是0吗,不能确定,应该优先root的父节点设置,所以得是2。

5、左1右2。root得是0,所以摄像头数++。

6、左2右2。root得是0,所以摄像头数++。

综上根据root的赋值,可以得到3种情况:

a、root是2:左1右1。即root没有被覆盖。不++,因为指望父节点设置摄像头

b、root是0:左右至少有一个2。即左右有一个没被覆盖,root这时候必须得设置了

c、root是1:剩余的都是这种情况(注意顺序)。

注意:

1、递归出口怎么写?叶子节点不放,所以遇 递归出口空节点 的时候,要保证叶子结点不会摄像头数++,而且叶子结点没有被覆盖,所以空节点要返回的是1。对应情况a。

2、最后还需要检查root是否被覆盖,因为情况a的话,root没有被覆盖不会result++,而是抛出2,但是上面没有节点了,所以在root这只能设置一个摄像头。

综上:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */




class Solution {
private:
    int result;
    int traversal(TreeNode* root)
    {
        if(!root)return 1;
        //0有摄像头、1没摄像头但被覆盖、2没被覆盖。
        int left=traversal( root->left);
        int right=traversal( root->right);
        if(left==1&&right==1)
        {
            return 2;
        }
        else if (left == 2 || right == 2) {
            result++;
            return 0;
        } 
        return 1;
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        if (traversal(root) == 2) { // 最后检查 root 无覆盖
            result++;
        }
        return result;
    }
};

贪心总结:

1、在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。要确定是排序哪一个维度。

在讲解本题的过程中,还强调了编程语言的重要性,模拟插队的时候,使用C++中的list(链表)替代了vector(动态数组),效率会高很多。

所以在贪心算法:根据身高重建队列(续集)

(opens new window)详细讲解了,为什么用list(链表)更快!

2、

关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。

上面的气球、无重叠区间、合并区间 的方法都比较像,用变量存储覆盖的边界。

3、贪心算法:最大子序和

其实是动态规划的题目,但贪心性能更优,很多同学也是第一次发现贪心能比动规更优的题目。

贪心算法:加油站

可能以为是一道模拟题,但就算模拟其实也不简单,需要把while用的很娴熟。但其实是可以使用贪心给时间复杂度降低一个数量级。

最后贪心系列压轴题目贪心算法:我要监控二叉树!

,不仅贪心的思路不好想,而且需要对二叉树的操作特别娴熟,这就是典型的交叉类难题了。主要体现在尽量选择父节点和状态的总结以及分类讨论.

  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在VS Code上刷力扣LeetCode)题目,首先需要进行以下几步操作: 1. 安装VS Code插件:在VS Code中搜索并安装LeetCode插件。这个插件可以提供LeetCode题目的在线编写和提交功能,以及自动测试和调试代码的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【史上最强代码编辑器VS Code】之VS Code刷力扣LeetCode)题目](https://blog.csdn.net/weixin_44553006/article/details/105183522)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [在 vscode 上刷力扣 Leetcode 可以这样来](https://blog.csdn.net/u012190388/article/details/121277555)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [leetcode答案-algo-study:从零开始刷力扣(LeetCode),JavaScript语言,算法](https://download.csdn.net/download/weixin_38680764/19920528)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值