IDEA
动态规划(从后向前)
时间复杂度 O ( n 2 ) O(n^2) O(n2)
class Solution {
public int jump(int[] nums) {
nums[nums.length - 1] = 0;
for(int i = nums.length - 2;i>=0;i--)
{
int temp = nums[i];
nums[i] = Integer.MAX_VALUE - 1;
for(int j = 1;j + i < nums.length && j<=temp;j++)
{
nums[i] = Math.min(nums[i],nums[i + j] + 1);
}
}
return nums[0];
}
}
贪心算法-从前向后的 O ( n ) O(n) O(n)解法
问题的最优子结构
- 假设最优解从下标为0的位置第一次跳到下标为
k
k
k的位置,则在该解中从下标为
k
k
k的位置跳到末尾的跳法一定是从下标
k
k
k跳到末尾的最优跳法。
- 可以使用剪切-粘贴技术证明,很容易。
贪心选择性质
- 使用局部最优解构造全局最优解
- 这里的局部最优解定义为:第一步从起始位置(即下标为0)跳到的位置满足其(“其”是从起始位置第一步跳到的位置,下面称为“该位置”)所能到达的位置距离起始位置(即下标为0)最远。
- 例如对于nums = [2,3,0,1,4]。起始位置下标为0,起始位置所能到达的位置(即该位置)可能为下标为1(对应nums[1] = 3),下标为2(对应nums[2] = 0)
- 对于下标为1的位置,最远能够跳到下标为4的位置,这个下标为4的位置距离起始位置距离为4
- 对于下标为2的位置,最远能够跳到下标为2的位置(因为nums[2] = 0,处在该位置一步也跳不走)。
- 因此根据上述定义,满足上述条件的位置下标为1而不是2,因为下标为1的位置所能跳到的最远距离比下标为2的位置跳到的最远距离长。
- 一定存在某个最优解的跳法的第一步为从起始位置跳到满足上述定义的位置。
- 如不然, 所有最优解的跳法的第一步都不是从起始位置跳到满足上述定义的位置。从中任取一个最优解,考察从起始位置第二次跳到达的位置(记下标为 q q q),它一定也可以由满足上述定义的第一次跳到达的位置(记下标为 p p p)跳到(否则这个第一次跳到的位置就不满足上述定义,即最远)。于是可以构造一个解,从起始位置跳到下标为 p p p的位置,再从下标为 p p p的位置跳到下标为 q q q的位置。
- 这个构造出来的解一定是最优解,否则任取的最优解就不是最优解了。
- 由于构造出来了最优解,而又假设了所有最优解的跳法的第一步都不是从起始位置跳到满足上述定义的位置,于是假设不成立,即原命题成立。
贪心算法(递归)
上述两个命题给出了求解此问题的贪心算法。
- 由起始位置求出满足上述定义的第一个跳到的位置,得出第一步跳法。
- 递归从这第一个跳到的位置开始,求出跳到末尾的最优解。
- 第一步跳法和递归求得的最优解的合并,就得到了原问题的最优解。
贪心算法(迭代-循环不变量-优化前的版本)
- 很容易将上述递归形式的算法转换为迭代形式
- 从起始位置开始,在起始位置能够跳到的位置范围中选择满足上述定义的位置
- 然后再从选择的那个位置选择其能够跳到的范围中满足上述定义的位置(这时这个选择的位置就应该是距离那个选择的位置最远的位置了)
- 以此类推
- 此外下面的代码还有些细节值得推敲
public int jump(int[] nums) {
int ans = 0;
int i = 0;
if(nums.length == 1)
return 0;
while(i < nums.length)
{
int end = i + nums[i]; //跳到的位置范围
ans++; //至少会跳1步
if(end >= nums.length - 1)
break;
int cursor = -1;
for(int k = i + 1;k<=end && k<nums.length;k++)
{
if(cursor == -1 || nums[cursor] + cursor < nums[k] + k)
cursor = k;
}
i = cursor; //保证了cursor一定会得到更新;
}
return ans;
}
- 分析:上述算法还不是
O
(
n
)
O(n)
O(n)的,因为还进行了不必要的重复遍历
- 还是考虑上述例子 n u m s = [ 2 , 3 , 0 , 1 , 4 ] nums = [2,3,0,1,4] nums=[2,3,0,1,4] ,我们第一步选择了从起始位置跳到下标为1的位置。然后又将下标为1的位置当作"新的起始位置"不断迭代。根据算法,这轮迭代将0也算在内。
- 但是0实际上是不需要在此轮迭代进行遍历的
- 因为从起始位置(下标为0的位置)可以跳到那个下标为2值为0的位置
- 所以最优解在第二步再跳到的位置不可能是那个下标为2的位置,因为我们实际上可以从起始位置经过一步就跳到那个下标为2的位置
- 一般的,将上述代码中加入pre_end,记录上次循环中的最右侧边界。下次迭代的for循环中从pre_end+1开始遍历,而不是i+1开始遍历。
- 每次for循环结束后,将pre_end就改成现在的end值
- 于是给出了 O ( n ) O(n) O(n)的算法代码如下
public int jump(int[] nums) {
int ans = 0;
int i = 0;
if(nums.length == 1)
return 0;
int pre_end = 0;
while(i < nums.length)
{
int end = i + nums[i]; //跳到的位置范围
ans++; //至少会跳1步
if(end >= nums.length - 1)
break;
int cursor = -1;
for(int k = pre_end + 1; k <= end; k++)
{
if(cursor == -1 || nums[cursor] + cursor < nums[k] + k)
cursor = k;
}
i = cursor; //保证了cursor一定会得到更新;
pre_end = end;
}
return ans;
}
简洁的官方代码
- 注意上述分析中,达到了pre_end
class Solution {
public int jump(int[] nums) {
int length = nums.length;
int end = 0;
int maxPosition = 0;
int steps = 0;
for (int i = 0; i < length - 1; i++) {
maxPosition = Math.max(maxPosition, i + nums[i]);
if (i == end) {
end = maxPosition;
steps++;
}
}
return steps;
}
}