[贪心_8] 跳跃游戏 | 单调递增的数字 | 坏了的计算器
目录
1.跳跃游戏
题解
2.单调递增的数字
证明
3.坏了的计算器
题解
解法一:正向推导
解法二:正难则反
1.跳跃游戏
链接: 55. 跳跃游戏
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
这道题和上一篇文章第一题,几乎一模一样,无非上面问的是跳到最后一个位置最小跳跃次数
这道题问的是能否跳到最后一个位置。
对步数 实现双指针 区间的维护模拟,来看 要走多少步
题解
一种 常见错误
完全参考 上篇文章的 的解法 2:利用层序遍历的过程
class Solution {
public:bool canJump(vector<int>& nums) {int left=0,right=0;int n=nums.size();while(left<=right){int tmp=right;for(int i=left;i<=tmp;i++){right=max(right,i+nums[i]);}left=tmp+1;if(right>=n-1) return true;}return false;}
};
2.单调递增的数字
链接: 738. 单调递增的数字
当且仅当每个相邻位数上的数字 x
和 y
满足 x <= y
时,我们称这个整数是单调递增的。
给定一个整数 n
,返回 小于或等于 n
的最大数字,且数字呈 单调递增 。
示例 1:
输入: n = 10
输出: 9
示例 2:
输入: n = 1234
输出: 1234
示例 3:
输入: n = 332
输出: 299
算法原理:
解法一:暴力解法 -> 暴力枚举
不是给了我们一个n,然后让找到小于等于n的最大数字,且数字是单调递增的。
所以我们可以从n枚举到0,只要找到数字是单调递增的,就返回。因为我们是从大到小枚举所以这个数一定是小于等于n并且是最大的那个数。
- 从大到小的顺序,枚举 [n,0] 区间内的数字
- 判断数字是否是 “单调递增的”
这里最主要的就是判断一个数是单调递增的。肉眼很好判断,但是让计算机不好判断,这里我们有两个常用方法:
第一种方法:我们遇到有个数字的时候,如果想处理某一位的时候,最常用的方式就是将数字转化为字符串。
比如要找数字中的每一位,如果单看数字1234你很难找每一位,但是我们可以将1234 转化为 “1234”,此时就可以用指针来遍历每一位
class Solution {
public:int monotoneIncreasingDigits(int n) {//倒着 遍历for(int i=n;i>=0;i--){string tmp=to_string(i);bool check=true;for(int j=1;j<tmp.size();j++){if(tmp[j]<tmp[j-1]){check=false;break;}}if(check) return i;}return 0;}
};
第二种方法:% 10 , / 10
prev记录之前%10得到的数字,cur记录/10之后然后当前%10得到的数字。
class Solution {
public:int monotoneIncreasingDigits(int n) {for(int i=n;i>=0;i--){bool check=true;int ans=i;while(ans){int tmp=ans%10;ans/=10;int cur=ans%10;if(cur>tmp) {check=false;break;}}if(check) return i;}return 0; }
};
这两种方法都会超时!时间复杂度是O(nlogn),O(logn)表示把数字中的每一位都提取出来时间复杂度是O(logn)
解法二:贪心(找规律)
假设有下面这样一个数,我们观察1-5是递增的,从5后面开始就是递减的。此时第一个贪心,如果前面的数是递增的我们会不会去修改它?肯定不会!修改高位势必会给高位某个数减小,影响太大了。
- 如果高位单调递增的话,我们不去修改
- 从5之后开始下降,我们最终要想找一个单调递增的话,调整一下后面的数使它从5开始递增并且尽可能的大,但是这个想法是实现不了的,你是会让4变成5,但是整个数相比之前就变大了。所以这个策略不行,调整4367使它从5之后开始递增是实现不了的。原因就是后面数的变化受到5的限制。如何解除这个限制呢?让这个5减小1变成4,然后的数都变成9,绝对是最大递增的。
2. 从左往右,找到第一个递减的位置,使其减小1,后面的数全部修改成 9
- 但是这里还有个问题,比如下面这个数,5是重复的,从左到右扫描到最后一个5的位置,但是执行刚才的策略是让最后一个5减少1,后面数都变成9,但是不行啊,你让最后一个5变成4,这个数就不是一个递增的了。其实我们应该调整第一个5变成4,后面的数都变成9
3. 从左往右,找到第一个递减的位置,从这个位置向前推,推到相同区域的最左段使其减小1,后面的数全部修改成 9
class Solution {
public:int monotoneIncreasingDigits(int n) {string ret=to_string(n);int sz=ret.size();for(int i=0;i<sz-1;i++){if(ret[i]>ret[i+1]){while(i > 0 && ret[i-1] == ret[i]) i--; // 回退到第一个递减位置ret[i]-=1;while(i<sz-1)ret[++i]='9';return stoi(ret);}}return n; }
};
证明
证明方法:反证法
假设贪心解是错误的,那必定会存在一个最优解,证明一下这个最优解是不存在的,那我们的贪心解就是最优的。
这里我们分开讨论,第一种贪心解得到的数和原数个数是匹配的,第二种个数不匹配。
- 先看第一种情况,假设贪心解不是最优解,那势必会存在一个最优解,最优解是严格大于贪心解并且是严格递增的。其次位数是一样的,贪心解位数都一样,最优解比贪心解大,位数肯定也是一样的。位数一样,从左扫描最优解肯定存在某一位是大于贪心解某一位的。
- 这里可以分为3个区域,递增区域,让原数减1区域,以及后面的区域。不过如果前两个区域都是一样的话,第三个区域肯定不存在比999还大的。因此我们只考虑前两个区域最优解的某个数大于贪心解
第一块区域,要么大于1、要么大于2、要么大于3,但是都是不存在的,因为这个数是单调递增的,最小的1333333都比原解还大了。
第二块区域,如果中间这个数比贪心解的这个数大最低就是4,但是也是不存在的,最优解也是一个递增的数如果这个是数4,后面即使全是4,最小的是1234444还比原数大,所以也是不存在的。
-
那后面的区域更别提了,不可能有大于999的数。所以说如果贪心解是错的,根本找不到一个最优解比贪心解大,所以说刚才的假设是错误的,因此我们的贪心解是正确的。 - 接下来我们看位数减少的情况,我们会发现位数减少的这个数正好是最大的减少位数中的最大数,你想找一个最优解比贪心解还大的情况那必定是6位数,如果是6位数还想保证比原数小,那这个数只能是1111111但是比原数大
因此这个最优解也是不存在的,所以我们的贪心就是最优的。
都找不到一个最优解大于贪心解。
3.坏了的计算器
链接: 991. 坏了的计算器
在显示着数字 startValue
的坏计算器上,我们可以执行以下两种操作:
- 双倍(Double):将显示屏上的数字乘 2;
- 递减(Decrement):将显示屏上的数字减
1
。
给定两个整数 startValue
和 target
。返回显示数字 target
所需的最小操作数。
示例 1:
输入:startValue = 2, target = 3
输出:2
解释:先进行双倍运算,然后再进行递减运算 {2 -> 4 -> 3}.
示例 2:
输入:startValue = 5, target = 8
输出:2
解释:先递减,再双倍 {5 -> 4 -> 8}.
题解
解法一:正向推导
以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。
1.end <= begin
奇数依旧+1,当end <= begin的时候,偶数此时就没有必要执行/2操作。
因为此时end都比begin小了难道你要/2之后在加1吗?那不如直接加1好了。
所以end <= begin,奇数+1,偶数+1,而且这个+1操作也不用一次一次算,我们仅需 begin - end就得到要执行几次+1了。
2.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 cnt=0;while(target>startValue){if(target%2==0)target/=2;elsetarget++;cnt++;}while(target!=startValue){target++;cnt++;}return cnt;}
};