一、概述
广为人知的跳台阶问题。
输入一个序列,从第一个开始跳,每次跳的最远距离是该元素的值,问最少跳多少次出队列。
这题目,本质上是将具体的跳台阶问题化为抽象的贪心问题。
我是在BFS→剪枝→找最大值→得到较优解中一步步理解的。
二、分析
我最终版本的代码和最优代码思想是一致的,但是我懒得从BFS改成循环了。就只分析我自己的代码,还可以从中看出我的分析过程。
第一眼看到这个题,我开始想到贪心,怎么贪心呢?先把最大值找出来,然后找次大值。。。。。。这样,发现不成,比如说最大值在最后一个,就几乎不可能用到这个值;再之后找“元素下标加元素值”的最大值,发现也不成,比如全是1,怎么找,就得一个一个遍历。
于是想到了BFS。
BFS是个什么思想呢?建树,以序列2、3、1、1、4为例,如下是建出来的树:
红色为跳跃值,蓝色为下标。最开始从0起步,下标为0的元素值为2,则可以跳1步或2步,因此有两个子树,分别跳到1和2;1的值为3,因此有三棵子树,2的值为1,只有一棵子树......最后当下标加元素值大于等于序列长-1的时候就找到了结果。结果为树的层数。
所以最开始我的代码就是传统的BFS:
class Solution {
int jump_next(vector<int>& nums,int nowLoc,int target)
{
queue<int> q;
q.push(nowLoc);
int nowjumps=1;
int last=1;//当前层最后一个元素所在位置
int cnp=1;//进入队列的节点总数
int cmp=0;//当前位置
while(!q.empty())
{
int tmp=q.front();
q.pop();
cmp++;
if(tmp+nums[tmp]>=target)
return nowjumps;
for(int i=nums[tmp];i>0;i--)
{
q.push(i+tmp);
cnp++;
}
if(cmp==last)
{
last=cnp;
nowjumps++;
}
}
return -1;
}
public:
int jump(vector<int>& nums) {
if(nums.size()==1)
return 0;
int res=jump_next(nums,0,nums.size()-1);
return res;
}
};
由于需要记录当前层数,所以需要维护多个变量,注意BFS中记录当前层数的做法:
维护三个变量:last、cnp、cmp。
last是当前层的最后一个节点的位置。cnp是当前入队元素的总个数。cmp是当前出队的是第几个元素。
第m层的第cmp个元素出队时,它的叶子节点全部入队,cnp随之增加,当cmp是m层最后一个元素时,也就是cmp等于当前last的值,cnp的值就是last应该更新的值,此时层数加一。
上面代码很容易看懂,就是把所有叶子节点全部入队,没有任何剪枝操作。很明显的,超时了。
然后我就开始思考具体的剪枝。
跳台阶本质上是什么呢?
我把它想象成是一个帝国,每跳一次台阶,帝国的势力就变大一点。
对,每跳一次台阶,帝国的势力变大一点。
也就是说,如果该次台阶不能让帝国的势力变大,那还不如不跳。
仍然以上面那幅图举例:
我们可以发现,在进行第一次跳跃时,帝国有两种选择:我这一步跳到1,下一步帝国势力最远跳到4;这一步跳到2,下一步帝国势力最远跳到3。可能有些人会担心这样一种情况:虽然跳到2看起来下一步跳的少,但是可能存在这样一条路径,选2比选1更好。
不存在的,因为跳到2之后的可选项(3)是跳到1后的可选项(2,3,4)的子集,所以跳到1是完全比2好的。也就是说,帝国做的选择取决于该选择的潜力,潜力如何量化呢?下标*元素值。
再来看1的三棵子树,分别能把帝国势力扩大到2,3,4,意思就是我到1了之后,能把帝国的范围最多扩大到4。然后看2,3,4三个节点,它们都只有一棵子树,分别能到3,4,5,也就是说,如果我下一步跳到4,那么我之后最远能到5,如果我跳到3,之后我最远能跳到4,如果我调到2,之后我最远能跳到3,即2,3,4的潜力分别为3,4,5,选择跳到潜力最大的4。
这样就找到最优路径了。也就是说,每层只要把“潜力”最大的入队即可。
优化后的代码如下:
class Solution {
int jump_next(vector<int>& nums,int nowLoc,int target)
{
queue<int> q;
q.push(nowLoc);
int nowjumps=1;
int last=1;//当前层最后一个元素所在位置
int cnp=1;//进入队列的节点总数
int cmp=0;//当前位置
while(!q.empty())
{
int tmp=q.front();
q.pop();
++cmp;
if(tmp+nums[tmp]>=target)
return nowjumps;
int max=tmp;//潜力初值
for(int i=nums[tmp];i>0;--i)
if(i+tmp+nums[i+tmp]>max+nums[max])//潜力比较
max=i+tmp;//潜力更新
q.push(max);//潜力最大的入队
++cnp;
if(cmp==last)
{
last=cnp;
++nowjumps;
}
}
return -1;
}
public:
int jump(vector<int>& nums) {
if(nums.size()==1)
return 0;
int res=jump_next(nums,0,nums.size()-1);
return res;
}
};
这样就把所有的无用枝都减掉了。
三、总结
最好的做法是DP,但是还没开始学,所以先不用。次好的方法用的思想就是我这个了。一步一步找到答案的感觉还是很不错的。