贪心算法专题(三)

文章目录

一、用最少数量的箭引爆气球

Leetcode链接

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

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

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

解题思路

  • points数组里放的是气球的宽度,但是没有给气球在y轴的上下位置,但是题目又说弓箭是垂直于x轴且可以向上一直走的,所以压根就不需要y坐标。要求每个弓箭尽可能多地引爆气球,那我们只要把箭射向多个气球重叠的位置就行了,我们发现这其实还是一个【区间问题】,气球宽度就是区间。
  • 我们还是先把区间按左边界排序,将区间排序之后有两个性质:
    ①:能够合并的区间(也就是之前【合并区间】那题,那些连续有交集的区间可以合并)是连续的,一旦中断,后面的区间就不会再和当前合并区间有交集了,性质①就是用来解决【合并问题】的
    ②:互相重叠的区间也是连续的,互相重叠是指一些区间之间任意两个区间都有重叠,这些区间就共同形成了一个"重叠堆",一旦后面有个区间不再和之前的区间一样满足这样的性质(不再属于这个重叠堆了),那它后面所有的区间也一定不会再属于这个重叠堆了,只能再形成一个新的重叠堆,这样一来排序之后区间就会呈先一个一个分开的"堆"(一个不和其他区间有交集的区间也算一个"堆")性质②就是用来解决【交集问题】的
  • 依据性质②,我们只需要统计一下有多少个这样的"堆"就行了,如何确定哪些区间是属于同一个"堆"的呢?其实和【合并区间】这题差不多,选一个起点(left,right),用它的 right 判断它和后面的待判区间(a,b)有没有交集,有交集就形成了一个"堆"(把交集部分叫做重叠区间),然后把right更新为重叠区间的右边界,这样如果下一个区间和重叠区间有交集(right>=a),那么它就和之前其他的区间都有交集;如果与这个待判区间没有交集就把right更新为b(并把这个新的"堆"计数),继续向后判断

代码实现及解析

java 复制代码
class Solution {
    public int findMinArrowShots(int[][] points) {
        //1.按左端点排序:
        Arrays.sort(points,(o1,o2)->o1[0]>o2[0]?1:-1);//端点值可能超出int范围,所以不要把他们相减,用三目表达式判断一下
        //2.统计"重叠堆"的数量
        int ret=1;//先把第一个重叠堆计数,下次++都是统计的新的堆,这样计数不会像之前那样把最后一次给漏掉了
        int right=points[0][1],n=points.length;
        for(int i=1;i<n;i++){
            int a=points[i][0],b=points[i][1];//先计算一下待判定区间的左、右边界
            if(right>=a){//与重叠区间有交集
                right=Math.min(right,b);//更新一下重叠区间的右边界即可
            }else{//没有交集
                right=b;//更换该待判区间的右边界为新的重叠区间边界,继续向后判断
                ret++;//并把这个新的重叠区间计数
            }
        }
        return ret;
    }
}

总结

  • 复习解题思路

二、整数替换

Leetcode链接

给定一个正整数 n ,你可以做如下操作:

如果 n 是偶数,则用 n / 2替换 n 。

如果 n 是奇数,则可以用 n + 1或n - 1替换 n 。

返回 n 变为 1 所需的 最小替换次数 。

解题思路

  • 可以用dfs算法试遍所有的选法,可以OC,但是本题还有个贪心的策略,不过就是要结合二进制数去发现很巧妙的规律,只能说当经验吸收吧
  • 首先偶数没的说,只能选 /2 操作,但如何确定奇数选择+1和-1哪个最优?
  • 奇数按其二进制数的最后两位数可分为两类,一类为"...01",一类为"...11":
  • 可以看到第一类为"...01"(前提n>1,就是保证省略号那部分中至少还有一个1)的选择-1操作后比选择+1后的二进制数中"1"的含量是要少的,这样一来它就可以在接近十进制的数字 1 的过程中处理更少的"1",就可以更多地使用 /2 更快的接近 "1D"
  • 第二类同理,"...11"(图中为方便展示用01111示例,实际只需要看最后两位分类)选择+1之后二进制数中的"1"含量是较少的,之后便于使用更多的 /2 接近"1D"(不是一次/2,省略号里面还有,反正比选择-1的那种二进制中的"1"少),n>3是因为,n=3是特殊情况,它选选择-1,再/2就到达"1D"了,更快

代码实现及解析

java 复制代码
class Solution {
    public int integerReplacement(int n) {
        int count=0;
        while(n>1){
            if(n%2==0){
                n/=2;//n是偶数的话只能选择/2
                count++;
            }else{
                if(n==3){//先处理一下特殊情况
                    count+=2;
                    return count;
                }else if((n&2)==0){//n的二进制的最后两位是01
                    n-=1;
                    count++;
                }else{//n&2==2:n的二进制的最后两位是11
                    //n+=1;这里非常恶心,如果n的值为int的最大值的话,+1正好存不下,直接完了
                    n=n/2+1;//但是有个很好用的技巧,如果再发现这种情况,但是+1和/2操作是一起的话(因为+1后变偶数,下一次一定是/2),
                            //就可以先/2,再+1,这样值不变,也不会导致溢出了
                    count+=2;
                }
            }
        }
        return count;
    }
}

总结

  • 复习解题思路和最后那个int溢出的注释

三、俄罗斯套娃信封问题

Leetcode链接

给你一个二维整数数组 envelopes ,其中 envelopesi = wi, hi ,表示第 i 个信封的宽度和高度。

当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算 最多能有多少个 信封能组成一组"俄罗斯套娃"信封(即可以把一个信封放到另一个信封里面)。

注意:不允许旋转信封。

解题思路

解法一(常规/通用解法):

  • 对乱序的信封数组,我们先按照左端点(宽度)进行排序,这样一来一封信想要找它可以套住的信封时,就只需要去前面找就行了,后面的信封宽度一定比它大,所以不行。这样这道题就变成了一道非常熟悉的题目了:【最长递增子序列】,本题是找到最长的大小严格递增的信封序列(也就是最多层的套娃),关键就是我们先按照一个维度的因素排好序了,这样就已经有助于题目的解答了,这样就和【最长递归子序列】一样只需要往前连接位置就行了
  • 解题思路基本上就是一样的,状态表示:dpi不是以ei为结尾的所有递增信封序列中,最长的递增序列的长度
  • 方法一会超时,因为题目的数据太大,但这不代表它不是一个值得尝试的算法,有可能其他题目就可以OC,算法本身很契合题目

解法二(重写排序+贪心+二分):

  • 解法二就是和【最长递增子序列】一样,使用贪心+二分进行优化,代码实现基本一样,就是数据从单个数变为了含有两个影响因素的信封(wide,height),但问题就在这,这种双影响因子的元素,在使用我们这个解题技巧(不同长度的子序列分类,保留每类中结尾最小的那一个)时,我们原来先是按照宽度升序排序,但是宽度一样的这些元素的排序是必须要继续处理的,不然不止一种情况下会导致逻辑出错,所以我们要重写排序:宽度一样我们要按照其高度继续进行降序排序,这样才不会出问题

  • 其实在处理这些元素时我们仍然是以一个数组的形式去看待它们的,但当我们按宽度升序+宽度相等时再按高度降序对信封排序后就不用再看宽度了:

  • 直接把宽度给删掉不看了,就只取高度的数据去实现代码,这样的话这题就真的完完全全变成【最长递归子序列】了,但是还是要注意一定要重写排序,建议就这样实现代码,要是将这些信封还是看做一个数组(wide,height)来处理的话会比较麻烦,还可能会出错

代码实现及解析

java 复制代码
class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        int n=envelopes.length;
        //1.排序:宽度不相等就按宽度升序,宽度相等的就按其高度降序
        Arrays.sort(envelopes,(o1,o2)->o1[0]==o2[0]?o2[1]-o1[1]:o1[0]-o2[0]);
        //2.贪心+二分
        ArrayList<Integer> hash=new ArrayList<>();//hash表储存筛选后那些信封的高度
        hash.add(envelopes[0][1]);
        for(int i=1;i<n;i++){
            int height=envelopes[i][1];
            if(height>hash.get(hash.size()-1)){//像【最长递增子序列】那样处理一下特殊情况
                hash.add(height);
            }else{
                //用二分找到height的插入位置:
                int left=0,right=hash.size()-1;
                while(left<right){
                    int mid=(left+right)/2;
                    if(hash.get(mid)<height){
                        left=mid+1;
                    }else{
                        right=mid;
                    }
                }
                hash.set(right,height);
            }
        }
        return hash.size();
    }
}

总结

  • 复习解题思路

四、可被三整除的最大和

Leetcode链接

给你一个整数数组 nums,请你找出并返回能被三整除的元素 最大和。

示例 1:

输入:nums = 3,6,5,1,8

输出:18

解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

解题思路

解法一:

  • 正着来是使用的dp算法,dpij表示前i个元素中选择,和为sum且sum%3==j(j∈0,1,2),sum的最大值

解法二:

  • 贪心算法是正难则反思想,我们可以先看一下nums的和sum是否满足sum%3= =0,满足的话就它了。如果不满足那就需要在nums中尽可能挑选一些较小的元素减掉使最终的sum%3==0:
  • 可以看到sum%3= =0不成立的话,只能两种可能:①:sum%3= =1;②:sum%3= =2;而对于两种情况产生的原因也是可分类谈论的(设xi满足xi%3==1,yi满足yi%3= =2):

①:

1.sum的组成:x1+其他,使得①成立

2.sum的组成:y1+y2+其他(y1<y2),使得①成立

②:

1.sum的组成:y1+其他,使得②成立

2.sum的组成:x1+x2+其他(x1<x2),使得②成立

  • 这样的话只需要找到数组中最小的两个x1、x2和两个y1、y2(按照优先级队列的逻辑自己实现一下就行),最后再分类讨论哪种情况就使用哪种推论,返回①/②中两个假设的max

代码实现及解析

java 复制代码
class Solution {
    public int maxSumDivThree(int[] nums) {
        int INF=0x3f3f3f3f,x1=INF,x2=INF,y1=INF,y2=INF;
        int sum=0;
        for(int num:nums){
            sum+=num;
            if(num%3==1){
                if(num<x1){
                    x2=x1;
                    x1=num;
                }else if(num<x2){
                    x2=num;
                }
            }else if(num%3==2){
                if(num<y1){
                    y2=y1;
                    y1=num;
                }else if(num<y2){
                    y2=num;
                }
            }
        }
        if(sum%3==0) return sum;
        else if(sum%3==1) return Math.max(sum-x1,sum-y1-y2);
        else return Math.max(sum-x1-x2,sum-y1);
    }
}

总结

  • 复习解题思路

五、距离相等的条形码

Leetcode链接

在一个仓库里,有一排条形码,其中第 i 个条形码为 barcodesi

请你重新排列这些条形码,使其中任意两个相邻的条形码不能相等。 你可以返回任何满足该要求的答案,此题保证存在答案。

示例 1:

输入:barcodes = 1,1,1,2,2,2

输出:2,1,2,1,2,1

解题思路

  • 怎么把这些数排列使得相邻的数不相等,直接给出思路,间隔插入法:统计出每个数出现的次数,并同时将出现次数最多的数maxNum以及该次数maxCount都记录,先把所有的maxNum从0下标开始每间隔一个位置进行放置,maxNum用完之后继续拿出其他统计出的一样的数按此规则继续放置,直到放到数组末尾。此时数组中的所有空位也都是间隔的,把剩下的数依次放入间隔(先放a个x1,再放b个x2...这样,只要同一种数一块放就行,不同类的顺序不需要关心)

为什么一定要先处理出现次数最多的数?

  • 假如用例为:1,2,2,如果先放1,那最后的结果还是1,2,2,所以必须先放出现次数较多的2:2,1,2

关于用例是否可以成功构建目标排列的证明(不过本题已注明用例均存在答案)

  • 把所有的数分为两两一组(不够两个的自成一组),总共可以分为(n+1)/2组,利用鸽巢原理可以证得:如果出现次数最多的那个数出现的次数不超过(n+1)/2的话就存在符合题意的排列,而如果maxCount>(n+1)/2的话,那至少存在一组中出现2个maxNum,就不存在符合题意的排列

代码实现及解析

java 复制代码
class Solution {
    public int[] rearrangeBarcodes(int[] barcodes) {
        HashMap<Integer,Integer> hash=new HashMap<>();
        int maxNum=0,maxCount=0;
        //记录每个数出现的次数的同时,把出现次数最多的数以及该次数都记录
        for(int x:barcodes){
            hash.put(x,hash.getOrDefault(x,0)+1);
            if(maxCount<hash.get(x)){
                maxNum=x;
                maxCount=hash.get(x);
            }
        }
        int index=0;
        //先把出现次数最多的数间隔一个位置进行放置
        for(int i=0;i<maxCount;i++){
            barcodes[index]=maxNum;
            index+=2;
        }
        //然后处理剩下的数,顺序随意
        hash.remove(maxNum);
        for(int key:hash.keySet()){
            for(int i=0;i<hash.get(key);i++){
                if(index>=barcodes.length) index=1;//index越界的话就重新回到前面第一个空位
                barcodes[index]=key;
                index+=2;//依次填放在空位上
            }
        }
        return barcodes;
    }
}

总结

  • 复习解题思路
相关推荐
super大力张35 分钟前
MOE基于结构的药物设计(七):Ligand R-Vectors——如何判断配体可以从哪里继续生长?
数据库·算法·cadd·moe·svl·基于结构的药物设计
Tongsr1 小时前
别只混淆代码:用 Kaleido 加固整个 Android Release AAB
前端·算法·github
高亦真1 小时前
今天是学习嵌入式的第34天
linux·学习·算法
飞Link3 小时前
动作方法中的分割与过度分割方法全解析(含代码实战与踩坑案例)
人工智能·python·算法·计算机视觉
张宇Joaquin3 小时前
鲲鹏统一并行加速库KUPL--异步数据拷贝
线性代数·算法·矩阵·numpy
GlobalInfo3 小时前
创新不是追逐热点,是在变化中建立长期竞争力
大数据·算法
lvwangshu3 小时前
线性代数基础 Linear Algebra Basics
数学·算法
ZhouDevin4 小时前
算法论文/数据集5——AlignPrune(CVPR2026)基于样本损失时间序列精确识别噪声样本
算法
十铭忘4 小时前
HMM(隐马尔可夫模型)的理解7——Baum–Welch 算法
人工智能·算法