最朴素的想法是,对于每个位置,挨个尝试一遍不同的跳法,这样总能找到最优解,最坏情况下A[i]=n,那么时间复杂度为O(n^2)。显然会超时,所以在这个朴素的算法上改进。
如果用动态规划求解,考虑如何划分子问题。一个很自然的想法是将起跳点为子问题边界,令p[i]表示从第i个位置起跳,到终点所需最小步数,则代价函数为p[i] = 1 + min{p[i + j]}, (1 ≤ j ≤ A[i])。再分析一下时间复杂度,对于每个p[i]需要循环A[i]次,一共有n个p[i],所以最坏情况下时间复杂度为O(n^2),可以看出,动态规划实际上跟朴素算法在时间复杂度上没有区别。
考虑能否使用贪心法,对于每个位置,尽量往最远的地方跳。显然这样只考虑了1跳的情况,比较"短视",很容易举出反例。
但是,可以把贪心法改进一下,考虑2跳的情况,即贪心的条件是:当前跳跃+下次跳跃的总距离最远。那这样是不是就可以了呢?答案是可以的。反证法:
假设当前由贪心法选择的方案不是最优解,那么一定存在一个最优跳法,它所需要的总步数更少,并且它至少在某一个位置下,当前一跳和下一跳(共2次跳跃)跳的不是最远的。考虑那个位置,如果按照贪心策略,那么得到的跳法(2次跳跃)的活动范围完全覆盖了最优解选择的跳法的范围。这样就保证了,贪心法一定不会错过某一个能跳的特别远的好机会。因此把这个位置的的跳法从最优解替换成贪心策略的跳法,总得跳数不会增加。这样总可以在有限的次数内,把最优解完全替换成了贪心法得到的解,且跳数跟最优解一样,这就跟最优解的定义矛盾了。
看一下贪心法的时间复杂度,虽然在每个起跳位置仍然要枚举A[i]次,但对于每个位置只遍历一遍,所以是O(n)。
回过头来想想:本题的特点是在每个步骤上,可选择的范围是连续的,从1到A[i]。正是这个条件导致了动态规划过于低效以及可以使用贪心法,A[i]表示在i处只能跳A[i]那么远,就不能用贪心法了,动归也不再是O(n^2)的了。
转化为代码时,实际上我们关心的只是前两跳最多能跳多远,所以用range保存第1跳能及的范围,next表示在此基础上第2跳能及的范围。当所处位置尚在第1跳范围内时,更新第2跳的范围,当所处位置超过了第1跳的范围,count加1,表示不得不跳一次,同时将第1跳的范围更新成原先第2跳的范围。如此迭代进行下去。
代码:(PS:i表示当前位置,是从1开始索引的)
1 int jump(int A[], int n) { 2 int range = 1; // 第1跳能及的最远范围 3 int next = 0; // 第2跳能及的最远范围 4 int i = 1; 5 int count = 0; 6 7 while (range < n) { 8 if (i <= range) { 9 next = max(next, i + A[i - 1]); 10 i++; 11 } 12 else { 13 count++; 14 range = next; 15 } 16 } 17 18 return count; 19 }