贪心算法六
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.坏了的计算器
题目链接: 991. 坏了的计算器
题目分析:
算法原理:
解法一:正向推导
以3转化成10为例,我们正向推导有两个操作x2,-1。
此时拿到3要么x2,要么-1,此时我们有一个小贪心的想法,为了更快到达,所以选择x2,得到6,这里也有两种选择要么x2,要么-1,如果延续刚才的贪心我们会x2,得到12,此时12比10大了,我们只能执行-1操作,得到11,再执行依次-1操作得到10,这里我们共进行了4次操作
但是我们发现如果再6的时候,不去x2,而去-1,得到5,然后在去x2,就得到10,总共才3次操作,反而比刚才的小贪心更优。
所以说在面临一个数的时候,是x2 还是 -1操作,判断标准其实是依赖后面的数来判断前面是x2还是-1好。所以说如果正向推导有点难,我们可以尝试另一个思路。
解法二:正难则反
我们这道题明显可能反着来做,x2和-1 操作 可以变成/2和+1 操作。所以说我们能正向从3推导到10,那肯定能逆向推导回去。
假设我们要从end转化成begin(10 -> 3)
正着难推导,难道逆着就好推导吗?确实是的,原因就在于这里/2操作,别忘了我们这道题是没有小数的,没有小数,那谁才能执行/2操作?那肯定是偶数,偶数/2能除尽,奇数/2除不尽。
所以面对奇数的时候只能执行+1操作,面对偶数我们在分情况讨论是/2还是+1。
- end <= begin
奇数依旧+1,当end <= begin的时候,偶数此时就没有必要执行/2操作。因为此时end都比begin小了难道你要/2之后在加1吗?那不如直接加1好了。所以end <= begin,奇数+1,偶数+1,而且这个+1操作也不用一次一次算,我们仅需 begin - end就得到要执行几次+1了。
- end > begin
奇数依旧+1,偶数要么/2,要么+1,此时这里我们有一个小贪心,因为end大于begin,我们想最少操作次数到达begin所以我们看似选择/2是更优的。
那这个贪心的想法对不对呢?
我们证明一下先除更优。
假设x是个偶数,并且大于begin。
如果不执行/2操作,而执行+1操作,那会得到一个奇数,但是奇数只能加1,然后又变成偶数了,又执行+1操作,又变成奇数,奇数只能+1,又变成偶数了。假设x一共加了k个1。这个k也是个偶数,如果k是奇数接下来还要+1总归要把它先加成一个偶数才行。
x + k 是大于 begin,必然会执行一次 / 2操作,你想把大的数变成小的数不执行除法操作怎么才能变小呢?所以无论加上多少1最终都会执行一次/2操作,变成(x+k)/2,次数这种操作执行的次数就是 k + 1次
但是如果拿x这个数变成(x+k)/2,先执行/2操作变成 x/2,然后仅需在x/2的基础上加上k/2,就得到(x+k)/2,这种执行操作次数是 1 + k/2次,是比上面执行次数小的。
所以说当 end > begin ,面对偶数我们也不需要分类讨论,仅需执行/2操作。奇数+1,偶数/2,一直到end <= begin的时候,执行begin - end就可以得到整个操作次数。
class Solution {
public:
int brokenCalc(int startValue, int target) {
// 正难则反 + 贪心
int ret = 0;
while(target > startValue)
{
if(target % 2) target++;
else target /= 2;
++ret;
}
return ret + startValue - target;
}
};
2.合并区间
题目链接: 56. 合并区间
题目分析:
给一个二维矩阵,每一行表示一个区间,里面有两个元素第一个表示左端点,第二个表示右端点,请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
有重叠的部分合并相当于就是求并集。注意示例2这个特殊的情况[1,4]和[4,5]也需要合并,也就是仅有一个点重叠也是可以合并。
算法原理:
关于贪心里面的区间问题的解法,我们都是固定的解法。
- 排序
- 根据排序后的结果,总结出一些规律,进而得出解决这个问题的策略
关于第2点也可以先提出一个解决问题的策略,进而总结出一些规律。
关于第1点排序,是分为两大类的,其中第一类是按照左端点排序,另一类是按照右端点排序。也就是把这些区间按照左端点或者右端点从小到大排序,原因就是不排序的话你去合并区间或者接近其他区间问题是要处理非常多的情况。但是先排序之后能让一些符合一些性质的区间是连续的,如果是连续的话,我们仅需遍历数组一遍。
左端点排序仅需关注左端点,右端点的大小不考虑。同理右端点排序仅需关注右端点,左端点的大小不考虑。
关于区间问题既可以用左端点排序也可以用右端点排序,只不过总结出来的规律不一样。但是都可以接近问题。这里我们只进行左端点排序,我们仅需用sort拍下序就可以了,它默认就是按照左端点排序。
解法 : 排序(左端点)+ 贪心策略
- 先按照左端点排序
排完序之后我们能得到非常重要的性质:能够合并的区间,都是连续的。
当第一次发现某个区间和前一个区间不能合并的时候,我们发现它后面的区间都是不能和这个区间合并的。这样我们仅需从左往右遍历数组一遍就可以把能合并的区间都给合并。
这道题明确告诉我们要合并区间了,但是有些区间问题会修改题干,我们要从中提取出来这道题是要我们合并区间的。这就需要我们的贪心思维了。
- 如何合并(求并集)
第二个能合并的区间类型以及不能合并的区间类型有如下:
前三类不管长什么样子都和第一个区间有重叠部分,最后一种情况可以归为没有重叠部分。其中我们重点关系第三类,有时候会分为有重叠部分,有时候会被分为没有重叠部分,具体看题意。
有重叠部分我们是要合并的,此时有个小贪心,我们合并完之后是要把这两个区间全都包含进去的。所以我们这里求的是并集。
有重叠部分的前提 a <= right,此时我们把这两个集合合并的时候,不管第2个区间是前三类的哪一种,合并之后 左端点依旧是left,右端点是 max(right, b)
没有重叠的前提,a > right,没有重叠的部分我们发现 left 和 right这个区间和后面区间都不会重叠,所以此时我们可以先把 [left ,right] 先放到结果数组ret里,然后把left更新成a,right更新成b,继续拿着新的区间去和后面的区间合并。
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 1. 先按照左端点排序
sort(intervals.begin(), intervals.end());
// 2. 合并区间
vector<vector<int>> ret;
int n = intervals.size(), left = intervals[0][0],right = intervals[0][1];
for(int i = 1; i < n ; ++i)
{
int a = intervals[i][0], b = intervals[i][1];
if(a <= right) // 有重叠部分
{
// 合并 - 求并集
right = max(right, b);
}
else // 没有重叠部分
{
ret.push_back({left,right});// 加入到结果中
left = a;
right = b;
}
}
// 别忘了最后一个区间
ret.push_back({left,right});
return ret;
}
};
证明:
排完序之后我们能得到非常重要的性质:能够合并的区间,都是连续的。
证明方法:反证法
原命题:按照左端点排完序 之后,能够合并的区间都是连续的
剩下的区间都是两两之间不可能在合并的。
假设合并完之后的区间能够和后面某个区间合并,我们仅需证明这种情况是不存在的即可。[a1,b1] 和 [a2,b2]是绝对不可能合并的,我们贪心策略从左往右只要有重叠部分就去合并,所以相邻的这两个区间是绝对不可能合并的。
根据相邻的不能合并和后面的合并我们可以得到两个不等式
b1 < a2
a3 <= b1
可以的 a3 <= b1 < a2
别忘记了我们是按照左端点排序
a1 < a2 <= a3,如果a1 <= a2 那就能合并了,所以 a1 < a2。a2和a3能不能合并不知道,我们正在证明这个区间和后面所有区间都不能合并,反证法是这个区间有可能和后面区间合并,所有a2和a3的关系不清楚。
根据这两个等式,我们会发现一个矛盾上面 a2 > a3,下面 a3 > a2,因此这种情况是绝对不会存在的。因此这种情况是绝对不存在的,只要按照左端点排序后,能合并的就合并后,然后两两之间是绝对不可能在重叠的。
3.无重叠区间
题目链接: 435. 无重叠区间
题目分析:
返回 需要移除区间的最小数量,使剩余区间互不重叠 。
看示例1,我们发现这道题和上一道题的区别,这一道题仅有一个点相交是不重叠的。
算法原理:
解法:排序(左端点)+ 贪心策略
- 按照左端点排序
当第一次发现某个区间和前一个区间不能合并的时候,我们发现它后面的区间都是不能和这个区间合并的。这样我们仅需从左往右遍历数组一遍就可以把能合并的区间都给合并。
2.移除区间
我们修改一下题干,题干要的是移除区间的最小数量,反过来就是尽可能保留更多的空间。
当我们把区间按照左端点从小到大排序之后,我们先考虑前两个区间,第一个区间和第二个区间有如下情况:
我们会把前两种归为一类,因为有一个端点相交属于不重叠。后面两个归为一类。
接下来我们以保留更多的区间来贪,当第一个区间和划线的区间你是会保留上面的区间,还是下面的区间?肯定是下面的!因为我们会拿保留的区间依次去和后面的区间比较,如果保留上面的区间它可能和后面的区间有更大概率重叠。因此我们会删除较长的区间,保留较短的区间和后面的区间做比较重叠的概率会低,那我就可以移除较少的区间,进而保留更多的区间。同理如果有重叠区间都这样做。
如果是这两个区间,删那个都是可以。原因在于别忘记我们是升序的,而且我们找是否重叠仅需判断a和right的关系,所以保留区间的左端点是谁都没关系,这里我们写代码就可以不管关心left了。
有重叠部分前提,a < right,移除右端点较大的区间,保留较小的区间,我们要right和b的较小值,和left没关系。
没有重叠区间的前提 a >= right,两个区间肯定不会傻乎乎的删除一个,此时上边的区间可以保留,然后拿下边的区间的right和后面的区间做比较。重复刚才的过程。
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
// 1. 按照左端点排序
sort(intervals.begin(), intervals.end());
// 2. 移除区间
int right = intervals[0][1], n = intervals.size(),ret = 0;
for(int i = 1; i < n; ++i)
{
int a = intervals[i][0], b = intervals[i][1];
if(a < right)// 有重叠部分
{
++ret;// 删除一个区间
right = min(right, b);// 贪心:删除右端点较大的区间
}
else// 没有重叠部分
{
right = b;
}
}
return ret;
}
};
证明:
当我们第一次遇到当前区间和后面一个区间没有重叠的部分的时候,它和更后面也是没有重叠的部分,这个我们在上一道题一句证明。所以这个区间也不需要在和后面区间比较了。
这里我们证明,我们的贪心策略是可以得到最优解的
证明方法:交换论证法
贪心解是两个区间做比较的时候,会把右端点较大的区间给删掉,保留较小的区间。
从左往右扫描第一个贪心解和最优解不一样的地方
最优解此时会把右端点较小的区间删除,保留较大的区间。那我们只要能把最优解调整成贪心解就可以说我们的贪心解是最优的。
假设最优解保留最长的,后面又出一个区间,此时会分为两种情况:
能重叠,无重叠。
先看无重叠的情况:最优解这里是一个都不删,贪心解保留的是较短的区间,较长的区间都和它不重叠,那较短的区间更不重叠。都是不删除所以最优解是可以调整成贪心解的。
能重叠的情况:最优解是必删一个,贪心解保留的较短区间和这个区间是有两种情况的,如果重叠就删,如果不重叠就保留。我们发现贪心解还比最优解更好,因此可以把最优解调整成贪心解。
所以后面遇到什么情况,我们都可以把最优解在不失去最优性的前提下调整成贪心解。因此贪心解是正确的。
4.用最少数量的箭引爆气球
题目链接: 452. 用最少数量的箭引爆气球
题目分析:
读完题目,我们其实只用关心气球的左端点和右端点就行了。这又变成了之前做的区间问题了。返回引爆所有气球所必须射出的 最小 弓箭数
注意示例3这里的细节问题,两个区间只要有一个端点重叠,这两个区间也属于重叠区间。
算法原理:
解法:排序(左端点)+ 贪心策略
- 按照左端点排序
先不管排序后有什么性质,我们先想一个贪心策略
- 提出贪心策略
要找最少弓箭数量 -> 一只箭应该引爆更多的气球 -> 将互相重叠的所有区间都拿出来引爆
互相重叠:两两之间是互相重叠的。此时仅需一只箭就可以把这些互相重叠的区间引爆。
此时我们按照左端点排序又会多了一个性质:按照左端点排序之后,互相重叠的区间是连续的,
能够合并和互相重叠不会一回事,互相重叠是更强的。
这三个区间能够合并是因为第二个区间,前两个区间合并之后然后能和第三个区间合并
如果是互相重叠,我们只能考虑前两个,前两个才是互相重叠,第三个只是和第二个重叠并不和第一个重叠。
互相重叠这个性质,大多数用来求交集
能够合并这个性质,大多数用来求并集
其实上面的题其实用的是求交集。
如何求出互相重叠的区间?
我们可以先求出前两个区间的交集,然后拿着这个交集和第三个区间做比较,如果交集和第三个区间有重叠的部分,那它们三个绝对是互相重叠的。
如何求交集?
还是和前面一样,不过这里我们可以把前三类放一块讨论,最后一类单独成一类。
前三类是重叠,最后一类不重叠。
重叠的前提 a <= right,此时我们要更新这两个区间内的交集,我们发现无论是前三类情况的哪一种,左端点求交集的时候必定要找较大的那个数,右端点求交集的时候必定要找较小的那个数。
但是我们上道题哪里已经说了,判断第三个区间到来是否之前的交集是否有重叠的部分,我们仅需考虑右端点就可以了。原因就是我们已经将区间按照左端点从小到大排序过了。因此求左端点没有意思,我们仅需考虑右端点就行了。 然后你会发现这里求min和上一道题无重叠区间是一模一样的,所有上一道题是
不重叠前提是a > right,
从左到右一直求交集,能合并就求交集,当第一次碰到不能合并的时候,说明前面仅需一只箭引爆,后面的还需在加一只箭,此时我们搞一个变量ret++,然后拿着新的区间继续和后面区间合并。然后就求出一个有多少个互相重叠的区间了。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
// 1. 按照左端点排序
sort(points.begin(), points.end());
// 2. 求互相重叠区间的数量
int n = points.size();
int right = points[0][1], ret = 1;
for(int i = 1; i < n; ++i)
{
int a = points[i][0], b = points[i][1];
if(a <= right)// 有重叠部分
{
right = min(right, b);
}
else// 无重叠部分
{
ret++;
right = b;
}
}
return ret;
}
};
证明:
证明方法:反证法
原:按照左端点排序之后,互相重叠的区间是连续的
反:按照左端点排序之后,互相重叠的区间不是连续的
如果区间不是连续的,根本不会有互相重叠的区间。