文章目录
一、用最少数量的箭引爆气球
有一些球形气球贴在一堵用 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;
}
}
总结
复习解题思路
二、整数替换
给定一个正整数 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溢出的注释
三、俄罗斯套娃信封问题
给你一个二维整数数组 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();
}
}
总结
复习解题思路
四、可被三整除的最大和
给你一个整数数组 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);
}
}
总结
复习解题思路
五、距离相等的条形码
在一个仓库里,有一排条形码,其中第 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;
}
}
总结
复习解题思路