力扣刷题记录-贪心算法相关题目

贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。

贪心算法是一种对某些求最优解问题的更简单、更迅速的设计技术。贪心算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,省去了为找最优解要穷尽所有可能而必须耗费的大量时间。贪心算法采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择,就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解。虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪心算法不要回溯

力扣 455. 分发饼干

原题链接

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        //从小到大遍历孩子数组,尽量用小的饼干满足孩子(贪心思想)
        Arrays.sort(g);
        Arrays.sort(s);
        //child用于遍历孩子数组,cookie用于遍历饼干数组
        int child=0,cookie=0;
        //饼干或者孩子遍历结束,就结束了
        while(child<g.length&&cookie<s.length){
            //当孩子胃口小于等于饼干大小,一个小孩被满足,数量+1
            if(g[child]<=s[cookie])child++;
            //饼干符合孩子胃口,看下一个饼干大小是否符合下一个孩子胃口;
            //饼干不符合胃口,看更大的饼干能否符合当前孩子的胃口
            cookie++;
        }
        return child;//输出被满足的孩子数量
    }
}

LeetCode 860. 柠檬水找零

原题链接

其实只需要考虑三种情况:
①顾客给出5元,直接收下;
②顾客给10元,收下一个10元(用于消耗),消耗1个5元;
③顾客给20元,优先消耗一个10元+一个5元(贪心所在),不行再消耗3个5元;

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five=0,ten=0;
        for(int bill:bills){
            if(bill==5)five++;
            else if(bill==10){
                if(five==0)return false;
                five--;
                ten++;
            }else if(bill==20){
                if(five>0&&ten>0){
                    five--;
                    ten--;
                }else if(five>=3)
                    five-=3;
                else return false;
            }
        }
        return true;
    }
}

力扣 376. 摆动序列

原题链接

在这里插入图片描述

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length <= 1) return nums.length;
        int curDiff = 0; // 当前差值(现在遍历到的数-前一个数)
        int preDiff = 0; // 前一对差值
        int res = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.length- 1; i++) {
            //计算当前峰值
            curDiff = nums[i + 1] - nums[i];
            //若当前差值和前一个差值为一正一负,则统计
            //若是前后差值正负情况一样,则不会统计(跳过),前一个差值保持在那
            //相当于逻辑上删除了数组元素
            if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
                res++;
                preDiff = curDiff;
            }
        }
        return res;
    }
}

力扣 53. 最大子数组和

原题链接

2023.12.04 四刷

贪心算法:
因为题目只要求返回最大和,所以用一个maxSum记录遍历过程中更新的最大的连续和(全局最优);使用count记录“连续和”(局部最优),如果加上当前遍历到的nums[i]后,count<=0(这时候前面那个区间的数只会成为“累赘”,拉低总和),则连续和区间需要更新,count=0,从num[i+1]开始重新累加;

class Solution {
    public int maxSubArray(int[] nums) {
        if(nums.length==1)return nums[0];
        int count=0,maxSum=Integer.MIN_VALUE;
        for(int i=0;i<nums.length;i++){
            count+=nums[i];
            maxSum=Math.max(maxSum,count);//更新全局最优
            if(count<=0)count=0;//更新局部最优
        }
        return maxSum;
    }
}

此题还有动态规划解法:

f(i)表示以nums[i]为结尾的“连续子数组的最大和”,所以只要求出每个nums[i]对应的f(i),返回最大的f(i)即可,但这样需要O(n)的空间,所以可以在计算出每个f(i)的同时,用一个maxSum来记录当前最大的f(i),这样遍历到最后输出maxSum即可,只需要O(1)空间复杂度。
而f(i)的递推关系式为:f(i) = max{ f(i−1)+nums[i] , nums[i] } = num[i]+max{ f(i-1) , 0 };

//动态规划
class Solution {
    public int maxSubArray(int[] nums) {
        int maxSum=Integer.MIN_VALUE;
        //pre相当于f(i),不过此题计算f(i)只需要知道f(i-1)与nums[i]即可
        //所以不用把所有的f(i)记录,只需记录前一个,然后更新覆盖即可
        int pre=0;
        for(int i=0;i<nums.length;i++){
            pre=nums[i]+Math.max(pre,0);//递推关系
            maxSum=Math.max(maxSum,pre);//取最大的
        }
        return maxSum;
    }
}

LeetCode 918. 环形子数组的最大和

原题链接

2024.03.10 一刷

本题为「53. 最大子数组和」的进阶版,设数组长度为 nnn,下标从 000 开始,在环形情况中,答案可能包括以下两种情况:
在这里插入图片描述
第一种情况的求解方法与求解普通数组的最大子数组和方法完全相同。对于第二种情况,我们可以找到普通数组最小的子数组 nums[i:j] 即可,因为当出现第二种情况时,中间的一定是最小的子数组,只要用total(全局总和)减去最小子数组,就可以得到全局最大子数组。

而求解普通数组最小子数组和的方法与求解最大子数组和的方法完全相同。

注意:如果数组中的元素全小于0,minSum将包括数组中的所有元素,导致我们实际取到的子数组为空。在这种情况下,我们只能取 maxSum作为答案。

代码如下:

class Solution {
    public int maxSubarraySumCircular(int[] nums) {
        int total=0;
        int minSum=nums[0];// 记录全局最小的 子数组
        int maxSum=nums[0];// 记录全局最大的 子数组
        int curMax=0;//统计以当前位置结尾的子数组最大值
        int curMin=0;// 统计以当前位置结尾的子数组最小值
        for(int num : nums){
            total += num;// 统计总和
            // 每遍历到一个位置,只要考虑要不要和前面连成子数组
            // 如果前面一段小于0(对当前num起负面作用),则以当前num作为起点,直接另起一段
            curMax = Math.max(curMax+num,num);
            maxSum = Math.max(maxSum,curMax);
            // 与上面统计以当前位置结尾的子数组最大值差不多
            // 只有前面一段比0小,才会和当前num连起来,记录以当前位置结尾的子数组最小值
            curMin = Math.min(curMin+num,num);
            minSum = Math.min(minSum,curMin);
        }
        if(total-minSum==0){
            return maxSum;
        }else{
            return Math.max(maxSum,total-minSum);
        }
    }
}

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

原题链接

因为要求的是最大利润,因此亏损的交易不用考虑(高位买入,低位卖出),又因为必须在买入后才能卖出,且只能买卖一支股票,因此只需要考虑低位买入高位卖出的情况。同时要明白,最后的最大总利润可以分解值每两天的差值累加(在剔除负利润前提下),例如第0天低位买入,第3天高位卖出:总利润=prices[3]-prices[0]=prices[3]-prices[2]+prices[2]-prices[1]+prices[1]-prices[0]。所以可以遍历价格数组,将当前遍历到的价格减去前一天价格(prices[i]-prices[i-1]),若为正利润,则可以累积到最终总利润中;

所用到的贪心思想是:
局部最优:每天的正利润
整体最优:最大总利润

//贪心思想
class Solution {
    public int maxProfit(int[] prices) {
        int res=0;
        for(int i=1;i<prices.length;i++)
            res+=Math.max(prices[i]-prices[i-1],0);
        return res;
    }
}

力扣 55. 跳跃游戏

原题链接

这题最开始没有理解好,所以解释一下。nums数组的每个元素代表在这个位置上,你最多可以跳几个单位,例如[2,3,1,1,4],nums[0]=2,说明在下标为0的位置可以跳1步,或者跳2步;nums[1]=3,说明在nums[0]处选择跳1步后到达nums[1],在nums[1]处可以选择跳1、2、3步,如果在这选择跳3步,就可以直接到达下标为4的nums[4]=4处,说明可以到达最后一个下标;

根据题意:
①可以设置一个当前可覆盖的范围coveRange变量,代表在当前位置的时候,跳跃当前位置的最大长度,可以到达的最远下标;
②将该值作为遍历范围,在遍历过程中实时更新该值;
③如果经过跳跃最大长度所能到达的最远下标还没超过当前的coveRange,那么就不用更新;
④判断coveRange是否超过nums.length,如果在遍历中超过,说明可以到达最远下标,返回true;遍历结束仍无法超过,说明无法到达,返回false

代码如下:

class Solution {
    public boolean canJump(int[] nums) {
        int coverRange=0;
        for(int i=0;i<=coverRange;i++){
            coverRange=Math.max(coverRange,i+nums[i]);
            if(coverRange>=nums.length-1)
                return true;
        }
        return false;
    }
}

力扣 45. 跳跃游戏 II

原题链接

这题相比于跳跃游戏,不同之处在于,此题需要求出最少跳几次可以到达最远下标。最重要的就是弄清楚统计的时候,在什么情况下步数count要+1,以及什么时候可以跳出对nums数组的遍历;
①步数count++的情况:当前遍历位置处于当前覆盖范围最远处,并且当前覆盖范围没有到达终点位置,count+1,跳跃一次,并且更新当前可覆盖范围;
②跳出对nums的遍历:i到达当前覆盖范围最远处,并且当前覆盖范围大于等于终点位置,不需进行下一跳,直接跳出循环,返回计数count;

class Solution {
    public int jump(int[] nums) {
        //若nums只有一个0元素,则当前位置即为最后一位,无需跳跃
        if(nums.length==1)return 0;
        int count=0;
        //当前下标可跳跃最远范围,下一跳可跳跃最远范围
        int curDistance=0,nextDistance=0;
        for(int i=0;i<nums.length;i++){
            //先更新下一跳的覆盖范围
            nextDistance=Math.max(nextDistance,nums[i]+i);
            //若走到当前覆盖范围的最远处的时候,需要分类讨论
            //即当前覆盖范围是否超出终点位置
            if(i==curDistance){
                //当前覆盖范围超过终点位置
                if(curDistance>=nums.length-1)break;
                else{//未超过终点位置
                    count++;//说明还要跳1次
                    curDistance=nextDistance;//并且跳完更新当前覆盖范围为下一跳覆盖范围
                    //这句不加也是正确答案,但是加了可以提前1步跳出循环
                    //并且注意,因为是通过对nextDistance进行判断
                    //所以这一句必须在当前位置count已经+1之后再出现,否则得到的count会比正确答案少1次
                    if(nextDistance>=nums.length-1)break;
                }
            }
        }
        return count;
    }
}

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

原题链接

这题要求翻转nums数组正负值k次,以求得最大数组总和;采用贪心思想:要使得总和最大,应该尽可能让数组中负数变正数,并且优先翻转绝对值大的负数;若是这样还没把k的次数用完,为使总和最大,第二步应尽可能让正数中绝对值小的进行翻转;

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        //直接对nums排序,每次都优先将最小值翻转
        //即存在负数时优先将最小负数翻转,都是正数时只翻转最小正数
        //这样按顺序进行可以保证每次都是最优的(尽可能让最后sum最大)
        Arrays.sort(nums);
        int minVal=101;//记录最小值
        int sum=0;
        for(int val:nums){
            //在k还有的前提,先翻转负数
            if(k>0&&val<0){
                val=-val;
                k--;
            }
            sum+=val;//
            minVal=Math.min(minVal,val);//记录最小值
        }
        //如果把数组遍历完都还有k,说明此时数组全是正数了,要消耗剩余的k
        //k如果是偶数个,翻转最小正数偶数次sum还是一样
        //k若是奇数的话,最小正数会变成负数,最终结果就是其它值总和-最小正数
        if(k>0&&k%2==1)sum=sum-2*minVal;
        return sum;
    }
}

力扣 134. 加油站

原题链接

局部->全局最优:

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        //curSum记录更新后的起点到当前位置的总剩余油量
        //totalSum记录从0开始到终点的总剩余油量
        int curSum=0,totalSum=0;
        int index=0;//记录更新的起点
        for(int i=0;i<gas.length;i++){
            curSum+=gas[i]-cost[i];
            totalSum+=gas[i]-cost[i];//累加每一站的剩余油量
            //若出现总剩余油量小于0情况,说明该区间油量不符合要求(从区间起点跑不到区间终点)
            //需要重置区间起点,并且区间剩余油量总和重新开始计算(贪心,局部最优)
            if(curSum<0){
                curSum=0;
                index=(i+1)%gas.length;//取模防止超出0~n-1范围
            }
        }
        //若从0开始到终点的剩余油量总和少于0,说明这个环路一定无法全部行驶(会缺油)
        //若没有返回-1,说明剩余油量总和大于0,则一定存在一个起点(就是index)
        if(totalSum<0)return -1;
        return index;
    }
}

力扣 135. 分发糖果

原题链接

对于这题,对同一个孩子不能同时考虑其左右两边的评分,否则在遍历过程会兼顾不了;可以第一次从左到右遍历评分数组,只考虑右边孩子比左边孩子的评分高的情况(右边糖果=左边+1);第二次从右到左遍历数组,只考虑左边孩子比右边孩子评分高的情况(左边孩子糖果=右边+1);这样每一轮遍历都可以保持“评分高的孩子获得更多糖果”的要求。

class Solution {
    public int candy(int[] ratings) {
        int[] candyVec=new int[ratings.length];
        candyVec[0]=1;
        //从左往右,右边大的+1;
        for(int i=1;i<ratings.length;i++){
            if(ratings[i]>ratings[i-1])candyVec[i]=candyVec[i-1]+1;
            else candyVec[i]=1;
        }
        //从右往左,左边大的+1或者保持不变
        for(int i=ratings.length-2;i>=0;i--)
            if(ratings[i]>ratings[i+1])
                candyVec[i]=Math.max(candyVec[i],candyVec[i+1]+1);
        int res=0;
        for(int num:candyVec)
            res+=num;
        return res;
    }
}

力扣 406. 根据身高重建队列

原题链接

注意审题,理解题意,题目给出的people数组是不按队列顺序排列的,要求我们在遍历people数组时,借助数组的hi和ki的信息,给这个数组重新排列,使之符合第i个人前面有ki个身高大于等于hi的人;

比如:[[7, 0], [6, 1], [7, 1]]

[7, 0] 前面没有比7大的,所以是0

[6, 1] 前面有一个身高7的,所以是1个

[7, 1] 前面有一个身高7的,所以是1个 即全都符合要求;

这题有点像前面的分发糖果,需要先对一个维度进行排序后,在不影响题目要求的前提下,再对第二个维度按一定规则进行排序;那么是先按身高排序还是先按编号ki排序呢,先按ki排的话,无论是从小到大还是从大到小,发现排序后的数组中,身高不符合题目要求,编号ki也不符合要求,显然是无用的;

应该先对身高进行排序,因为编号ki的规则是:第i个人前面有ki个身高比它高的人;所以按身高降序排列(从高到低可以使编号k更小、身高也更矮的插入前面队列,也不会影响已经插入好的前部分队列,因为后面的人只要能够保证前面比它高的人有ki个,更矮的插入前面不会影响这个规则),这样只需要再按编号k为下标,重新插入队列即可;

例:people=[[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]

按身高降序排序后:
[ 7, 0 ], [ 7, 1 ], [ 6, 1 ], [ 5, 0 ], [ 5, 2 ], [ 4, 4 ]

第二个数字作为索引位置,把数组放在目标索引位置上。如果原来有数了,会被往后挤

[[7, 0]]

[[7, 0], [7, 1]]

[[7, 0], [6, 1], [7, 1]]

[[5, 0], [7, 0], [6, 1], [7, 1]]

[[5, 0], [7, 0], [5, 2], [6, 1], [7, 1]]

[[5, 0], [7, 0], [5, 2], [6, 1], [4, 4], [7, 1]]

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        //对people数组进行排序,按身高h进行降序排列
        //在身高相同时,按k进行升序排列
        //Lambda表达式重写排序规则
        Arrays.sort(people,(int[] a,int[] b)->{
            if(a[0]==b[0])return  a[1]-b[1];//第二个元素升序
            return b[0]-a[0];//第一个元素降序
        });
        
        List<int[]>res =new ArrayList<>();

        //用i[]来遍历重新排序好的二维数组people的每一行(一行两个数字)
        //在res按编号k从头开始插入people数组对
        for(int[] i:people)res.add(i[1],i);

        //将List<int[]>类型的res转换为二维数组,二维数组长度为people.length
        return res.toArray(new int[people.length][]);
        //集合只能转换成引用类型的数组,包括字符串数组、包装类数组,不能转换成基本类型数组,但是可以转换成基本类型的二维数组。
    }
}

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

原题链接

2023.12.05 二刷

题意就是给出一批气球在x轴上的宽度范围,只要箭射出的位置在气球x坐标范围内,可以射爆,不论多少数量,要求使用尽可能少的箭射爆尽可能多的气球;

很容易想到需要对数组排序,按左边界升序排序后,该如何安排在哪里射箭呢?

从图中可以看出,如果气球重叠,能够在一批气球中一起射爆的气球需要保证这样的规则:其它气球的左边界一定要小于等于这批气球中的最小右边界。如果当前遍历到的气球左边界大于这一批的最小右边界,那么从当前气球开始,就需要用一支新的箭了(count++)。

举例:
可以看出首先第一组重叠气球最小右边界为6,其它所有左边界小于6的都可以和它分在一组,这一组一定是需要一个箭;气球3的左边界7大于第一组重叠气球的最小右边界6,所以再需要一支箭来射气球3了。

(图片来自代码随想录):
在这里插入图片描述

class Solution {
    public int findMinArrowShots(int[][] points) {
        //给气球按左边界从小到大排序
        //测试用例中有极限值,不能用减法方式进行排序
        Arrays.sort(points,(int a[],int b[])->Integer.compare(a[0],b[0]));

        //至少一个气球
        int count=1;

        //i-1个和i进行比较判断,所以从i=1开始
        for(int i=1;i<points.length;i++){

            //当前气球左边界和前一个气球右边界进行比较
            //如果超过,说明前面那一批气球必须用掉一支箭才可以射爆
            if(points[i][0]>points[i-1][1])
                count++;
            //否则说明当前气球左边界在一支箭可以射爆范围
            else 
           		//那么实时更新这一批可以射爆的气球的最小右边界
                points[i][1]=Math.min(points[i][1],points[i-1][1]);
        }
        return count;
    }
}

值得一提的是,这里面用到的方法的Integer.compare的原理是这样的:

//compare
public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

对于输入的x,y:

若x<y,则返回-1(小于0的数);

若x=y,则返回0;

若x>y,则返回1(大于0的数);

在源码中,如果调用compare方法返回值大于0,就把前一个数和后一个数交换,也就是把大的数放后面了,即所谓的 升序了;如果将x,y顺序调换,就是降序了

返回值小于等于0的时候,保持前后顺序不变;

力扣 435. 无重叠区间

原题链接

2023.12.05 二刷

这题要求最少移除几个区间,可以形成几个互相不重叠的区域,也就是要使剩余的不重叠的区域最多;如果能求出不重叠区域最多有几个,最后:移除区间数=总区间数-不重叠区域数;

贪心策略:目的是求出尽可能多的不重叠的区域

将所有区间按右边界从小到大排序,选取右边界最小的作为第一个划分区间,这样可以使剩余的区域最大(局部最优),有更大的可能容纳更多的区间;后面的划分遵循这个规则,从而使最终的不重叠区间数量最多(整体最优);

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        //按区间的右边界,从小到大排序
        Arrays.sort(intervals,(int[] a,int[] b)->Integer.compare(a[1],b[1]));
        //两种写法都行,当有溢出风险时选择第一种写法
        // Arrays.sort(intervals,(int[] a,int[] b)->{
        //     return a[1]-b[1];
        // });
        int count=1;//记录不重叠区间的个数(至少为1)
        int end=intervals[0][1];//记录区间分割点
        //i=0的区间已经记录(右边界最小的区间),所以从i=1开始
        for(int i=1;i<intervals.length;i++){
            //将当前区间左边界与前一个分割好的区间有边界比较
            //大于等于即可新开一个区间
            if(intervals[i][0]>=end){
                ++count;
                end=intervals[i][1];//更新
            }
        }
        //count是记录最多可以分成几个不重叠区间;
        //则最少可以删除的区间数=总区间数-剩余不重叠区间
        return intervals.length-count;
    }
}

这题其实和力扣 452. 用最少数量的箭引爆气球是类似的,弓箭数量相当于不重叠区间数量,只要在射爆气球的if判断中加个等号([0,1][1,2]是两个区间,即需要两支弓箭才可以射爆这两个区间),最后用总区间数减去弓箭数量就是要移除的弓箭数量。

在这里插入图片描述

代码如下:

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        Arrays.sort(intervals,(int a[],int b[])->Integer.compare(a[0],b[0]));
        int count=1;
        for(int i=1;i<intervals.length;i++){
            if(intervals[i][0]>=intervals[i-1][1])
                count++;
            else 
                intervals[i][1]=Math.min(intervals[i][1],intervals[i-1][1]);
        }
        return intervals.length-count;
    }
}

类似的还有605. 种花问题455. 分发饼干、253.会议室II(力扣会员题);

力扣 763. 划分字母区间

原题链接

这题的要求:

①把字符串划分出尽可能多的片段;

②同一个字母最多出现在一个片段;

③返回一个片段长度的列表;

由于同一个字母只能出现在同一个片段,显然同一个字母的第一次出现的下标位置和最后一次出现的下标位置必须出现在同一个片段。因此需要遍历字符串,得到每个字母最后一次出现的下标位置。

然后重新遍历字符串,如果当前下标就是这个字母在字符串中出现的最后一次,就进行分割,这样可以保证每个片段的长度一定是符合要求的最短长度,如果取更短的片段,则一定会出现同一个字母出现在多个片段中的情况;如果取更长片段,就不符合题目要求的“分割成尽可能多的片段”;由于每次取的片段都是符合要求的最短的片段,因此得到的片段数也是最多的。

class Solution {
    public List<Integer> partitionLabels(String s) {
        List<Integer> res=new ArrayList<>();
        //存储字符串中每个字符字母最后分别在哪个位置出现
        int[] hash=new int[26];
        for(int i=0;i<s.length();i++){
            hash[s.charAt(i)-'a']=i;
        }
        
        //left和right分别为划分的片段的左右边界下标
        int left=0,right=0;
        //重新遍历字符串
        for(int i=0;i<s.length();i++){
            //找到当前字母在字符串中最后出现的位置
            right=Math.max(right,hash[s.charAt(i)-'a']);
            //如果当前下标就是字母在字符串最后出现的位置
            //则按左右边界进行分割,并且更新左边界为下一个下标
            if(i==right){
                res.add(i-left+1);
                left=i+1;
            }
        }
        return res;

    }
}

力扣 56. 合并区间

原题链接

思路:

本题是判断区间重贴后要进行区间合并。

所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。

按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)

图片来自代码随想录:
在这里插入图片描述

2023.12.05 三刷

// 贪心思想,时间O(nlogn)(排序所用时间+遍历一遍intervals),空间O(logn)(排序所需空间)
class Solution {
    public int[][] merge(int[][] intervals) {
        List<int[]>res=new ArrayList<>();
        
        //按区间左边界升序排序(lambda表达式)
        Arrays.sort(intervals,(o1,o2)->{
            return o1[0]-o2[0];
        });

        //区间的起始左右边界
        int start=intervals[0][0];
        int end=intervals[0][1];

        for(int i=1;i<intervals.length;i++){
            //当前遍历到的区间左边界大于上一个区间的右端点
            //说明它们不重叠,可以把上一个区间加入res链表
            //然后更新当前区间为待加入res的准备区间
            if(intervals[i][0]>end){
                res.add(new int[]{start,end});
                start=intervals[i][0];
                end=intervals[i][1];
            }else{
                //若当前遍历到的区间左边界在上一个区间内部
                //则把这个区间合并到上个区间内(更新上个区间的右边界)
                end=Math.max(end,intervals[i][1]);
            }
        }
        //注意res.add这个操作是发生在intervals[i-1][1]作为右边界时的
        //所以for循环结束之后还需要补上最后一个区间
        res.add(new int[]{start,end});

        return res.toArray(new int[res.size()][2]);
    }
}

(图片内容转自力扣一位大佬的题解):

lambda表达式:
在这里插入图片描述

res.toArray():

在这里插入图片描述

力扣 738. 单调递增的数字

原题链接

对于这题,最容易想到的就是暴力解法了,也就是从n开始递减,每-1就判断一下当前数字是否符合题目要求的单调递增数字的定义,但是这样做的时间复杂度是O(n*m),m是n这个数字的长度;但是因为n数量级是10^9,所以暴力解法不可行。

那么该怎么做这题呢,这个数量级显然是需要进行一次遍历,在遍历过程中调整n的每一位大小,找到符合条件的最大值;

那么是要从前向后遍历还是从后向前呢,显然是要在遍历过程中,遇到前面的数字比后面数字大的情况时,前面的数字要-1,然后后面的那位数字调整为9;

举例:97
str[0]>str[1],需要让str[0]–,然后再让str[1]=9,这样就变成89了,就是符合要求的最大单调递增数字;

换成贪心思想的表述就是:

局部最优:遇到str[i-1]>str[i],让str[i-1]–,然后str[i]=9,这样可以保证这两位变成最大的单调递增数

全局最优:从右向左遍历(从左向右的话遇到类似332这样的数,会变成329,这时“32”不符合要求),一直都是最优的结果。

//自己的解答,逻辑比较清晰,但是
class Solution {
    public int monotoneIncreasingDigits(int n) {
        //int->String->char[]
        char[] nums= String.valueOf(n).toCharArray();
        int startIndex=10;//记录从哪里开始后面全变成9

        for(int i=nums.length-1;i>0;i--){
            if(nums[i-1]>nums[i]){
                nums[i-1]--;//前一位需要-1
                startIndex=i;//更新9出现的位置下标
            }
        }

        for(int i=0;i<nums.length;i++)
            if(i>=startIndex)nums[i]='9';

        //char[]->String->int
        return Integer.parseInt(new String(nums));
    }
}


//官方题解,时间复杂度更好,只要遍历一次字符数组
class Solution {
    public int monotoneIncreasingDigits(int n) {
    
        char[] strN = Integer.toString(n).toCharArray();
        
        int i = 1;
        //从左到右找到第一个需要改为9的位置
        while (i < strN.length && strN[i - 1] <= strN[i]) {
            ++i;
        }
        
        if (i < strN.length) {
            while (i > 0 && strN[i - 1] > strN[i]) {
                --strN[i - 1] ;
                //这一句必须加,要从不符合递增的位置,一步步回退
                //检查strN[i-1]-1后,前面是否会不符合单调递增规则
                //不符合的话继续在while中调整
                --i;
            }
            //由于前面while中最后一步多减了一个1,所以下标要+1才是修改9的位置
            for (i=i+1; i < strN.length; ++i) {
                strN[i] = '9';
            }
        }
        
        //char[]->String->int
        return Integer.parseInt(new String(strN));
    }
}

力扣 714. 买卖股票的最佳时机含手续费

原题链接

这题相比122. 买卖股票的最佳时机 II多了一个手续费,在122中,只需要收集正利润即可;但是对于本题,还需要考虑买卖的利润是否大于手续费,如果进行买卖后利润小于等于手续费,明显没必要进行交易;手续费可以算在买入的时候,也可以算在卖出的时候,不过为了后面解题便于理解,统一设置买入价格含手续费fee,即设置一个buy=prices[0]+fee作为初始的买入价格( buy表示在最大化收益的前提下,如果我们手上拥有一支股票,那么它的最低买入价格是多少),后续碰到更低买入价格的时候就更新买入价格

贪心的思想就是最低的时候买入最高的时候卖出(算上手续费还能盈利的前提),并且在最高处进行利润累加;

这题主要就是找出什么时候买,什么时候卖,并且还需要考虑,在今天卖了之后,后面如果出现更高的价格该怎么处理,比如prices = [1,4,3,8],fee=2,在价格为4那天,是直接卖了盈利(1买4买,3买8卖,利润4),还是继续持有到8那天再卖(1买8卖,利润5),这样看来,在收手续费的条件下,肯定是买卖次数更少比较好,。

但是,我们在遍历的过程中,是没办法知道后面的天数里,会不会出现比今天更高的价格的,那么如何判断今天是继续持有还是直接卖出呢呢?

其实可以先“假装卖出去”,将这次的利润累加进入结果(profit+=prices[i]-buy),但是需要记录下这次卖出的价格(buy=prices[i]):

①假设k天后出现了更高的卖出价格(prices[i+k]>buy),那么我们会获得 prices[i+k]−prices[i]的收益,加上这一天 prices[i]−buy的收益,恰好就等于在这一天不进行任何操作,而在后面第k天卖出股票的收益;

②如果后面第k天的买入成本比今天更低(prices[i+k]+fee<buy),那么与其使用 buy的价格购买股票,我们不如以 prices[i+k]+fee 的价格购买股票,因此我们将 buy=prices[i+k]+fee;而这次的交易直接“当真”(反正已经把利润累加进结果了)

③对于其余的情况,prices[i] 落在区间 [buy−fee,buy] 内,它的价格没有低到我们放弃手上的股票去选择它,也没有高到我们可以通过卖出获得收益,因此我们不进行任何操作。

力扣官方题解总结:上面的贪心思想可以浓缩成一句话,即当我们卖出一支股票时,我们就立即获得了以相同价格并且免除手续费买入一支股票的权利,即代码中的buy = prices[i]这一句。

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int profit=0;
        //记录最低买入价格
        int buy=prices[0]+fee;
        for(int i=1;i<prices.length;i++){

            //情况1
            if(prices[i]+fee<buy)buy=prices[i]+fee;
            
            //情况2
            if(prices[i]>buy){
                profit+=prices[i]-buy;
                buy=prices[i];
                //注意这里不要+fee,这是因为这里的buy用于应对两种情况
                /**
                1.如果后面的价格比这次更高(情况2):那么这个buy就可以用于“反悔”,即可以把这次卖出当做没有发生,在更高价格那次进行profit+=prices[i+k]-buy(这里假设k天后出现的价格比这次更高),因为在这次里累加的利润已经算了手续费fee,在更高价格那次就不应该算手续费,而是直接加上prices[i+k]-prices[i];
                2.如果后面的价格prices[i]比这次更低(情况1):那么这次交易就当做已经完成,直接更新后面的最低买入价;
                 */
            }
        }
        return profit;
    }
}

力扣 968. 监控二叉树

原题链接

本题既可以用动态规划,也可以用贪心思想,不过这题使用贪心思想的代码会更容易理解,代码也更简洁。

由示例图片可以看出,示例中的摄像头都没有放在叶子节点上,理解题意后,也可以想到,对于一棵二叉树,如果给结点安装摄像头,监控其它结点,那么叶子结点是尽量不安装摄像头的,而是应该把摄像头安在其父节点上。

针对输入的二叉树,从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少。

我们需要输入二叉树进行后序遍历得到从下往上的迭代,并根据左右子节点的状态关系,对父节点的状态进行更新。

我们将状态关系列为如下三种,即
0:没被监控到
1:安装摄像头
2:被监控到

因此存在如下三种子节点情况:
情况1:左右孩子皆被监控到(left2&&right2),出于使总摄像头数量更少的目的,当前节点可以不安摄像头(也就是处于没被监控到的状态,return 0),因为当前结点可以交由上一层父结点监控;如图:
在这里插入图片描述
情况2:左右子节点至少有一个没被监控到(left0||right0),为了保证它可以被监控,则当前结点需要设置摄像头(res++;return 1;);
包含这些状态:(left,right)->{(0,0),(0,1),(1,0),(0,2),(2,0)}

情况3:左右子节点至少有一个摄像头(left1||right1),则当前结点为被监控状态(return 2),无需摄像头。
包含这些状态:(left,right)->{(1,1),(1,2),(2,1)}

在进行后序遍历的过程中,还需要注意两点:

1.空节点视为什么状态,应该怎么处理?

和空节点直接联系的是叶子结点,一开始的时候叶子结点肯定是没有被监控的状态0;为了使摄像头最少,摄像头应该设在叶子结点的父节点处。空节点状态只能从3种状态选择一个:

状态0–没被监控状态:这样到了叶子结点(left,right)->(0,0),return 1(摄像头);叶子结点就要设置摄像头了,所以不行;

状态1–设置摄像头:叶子结点处(left,right)->(1,1),return 2(被监控状态),这样虽然叶子结点是被监控了,但是是被空节点监控,这样叶子结点的父节点就没必要设置摄像头了,不符合贪心规则;

状态2–被监控状态:叶子节点处(left,right)->(2,2),return 0(没被监控状态);符合规则,所以空节点状态应该返回状态2;;

2.对整棵树根节点的处理:

由于是自底向上的遍历,每个当前结点只负责保证它下面的结点能被监控到,对于它自身能否被监控,是交给它的上层父节点进行的;所以这就会出现一个问题,在情况1中,对于根节点而言,如果它左右孩子都被监控到(l=2,r=2),在一般规则下,它应该返回0(没被监控),但是这样的话它无法让自己被监控到。所以在遍历的最后,如果根节点返回的是0,那么它应该也安装摄像头(res++)。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res;
    public int postorder(TreeNode root){
        //递归终止条件,碰到空节点,设置成被监控状态
        if(root==null)return 2;
        //记录左右孩子的状态
        int left=postorder(root.left);
        int right=postorder(root.right);
        //左右子节点只要有一个没被监控到,当前结点就要安摄像头
        if(left==0||right==0){++res;return 1;}
        //左右子结点只要有一个有摄像头,当前结点就是被监控状态
        else if(left==1||right==1)return 2;
        //只剩下(2,2)的情况了,即左右孩子都是被监控,则当前结点不安摄像头
        //交由父节点安装
        else return 0;
        
    }
    public int minCameraCover(TreeNode root) {
        res=0;
        //最后对头结点再判断下
        if(postorder(root)==0)++res;
        return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值