一.跳跃游戏简单介绍

1. 跳跃游戏简单介绍

跳跃游戏是一种典型的算法题目,经常是给定一数组arr,从数组的某一位置i出发,根据一定的跳跃规则,比如从i位置能跳arr[i]步,或者小于arr[i]步,或者固定步数,直到到达某一位置,可能是数组的最后一个位置,也有可能是某一特别的数值处。

在上一期有可以用用贪心、动态规划、dfs解决的题目,这一期主要关注在跳跃游戏中就经常使用的动态规划剪枝、前缀和、滑动窗口和BFS,由于在大部分情况下,能用DFS解决的题目都可以用BFS解决,且两种方法有基本相同的复杂度,尤其是在跳跃游戏这类题目中,可以视为一种方法。

2. 经典问题回溯:青蛙跳台  与  爬楼梯最小成本

青蛙跳台与爬楼梯是跳跃游戏大类中最为经典的题目,其解题思路也是跳跃游戏中最为核心的解决问题的方法。

(1)leetcode70 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1或 2个台阶。你有多少种不同的方法可以爬到楼顶呢?

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

动态规划

class Solution {public int climbStairs(int n) {if(n <= 1) return n;int[] dp = new int[n+1];dp[1] = 1;dp[2] = 2;for(int i = 3; i <= n; i++){dp[i] = dp[i-1] + dp[i-2];}return dp[n];}
}

解析:当我们来到第i个楼梯,我们有可能从哪里上来的?那么答案只有两个,从i-1处蹦一格,从i-2处蹦两格,以f(i)代表在当前格存储的方法数目,即:

我们可以得到动态规划的递推公式:

注意边界条件,在i <= 2时,我们直接进行返回就好。

总结

动态规划的初始条件: dp[1] = 1,dp[2] = 2,由题目简单可得
动态规划的递推公式:f(i) = f(i-1) + f(i-2)
动态规划的终止条件:来到数组最后一个位置即可

 滚动数组优化

class Solution {public int climbStairs(int n) {if(n <= 2) return n;int num1 = 1;int num2 = 2;int num3 = 3;for(int i = 3; i <= n; i++){num3 = num1 + num2;num1 = num2;num2 = num3;}return num3;}
}

从上述动态规划的做法来看,我们可以节省空间,进行滚动数组的优化,因为f(i)的值只与f(i-1)与f(i-2)有关,即与当前需求变量不相关变量可以用完就扔。

用num1与num2进行替代f(i-1)与f(i-2),随着遍历交替前进。最后在num3中保存的就是最终需要的值。

(2)leetcode剑指 Offer 10- II. 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

输入:n = 2
输出:2输入:n = 7
输出:21

本题与leetcode70 爬楼梯是同一个问题,但是由于整数取值的范围较大,导致益出,需要取余,在循环中进行取余即可,用滚动数组的方法比较方便。

​
class Solution {public int numWays(int n) {if(n <= 1) return 1;int num1 = 1;int num2 = 1;int num3 = 2;for(int i = 2; i <= n; i++){num3 =  (num1 + num2) % 1000000007;num1 = num2;num2 = num3;}return num3;}
}​

(3)剑指 Offer II 088. 爬楼梯的最少成本

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。

请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

此题和 (1)和(2)是同一个类型的题目,但是需要考虑成本的问题,即在动态规划的递推公式中加上成本项。

class Solution {public int minCostClimbingStairs(int[] cost) {int[] dp = new int[cost.length+1];dp[0] = 0;dp[1] = 0; for(int i = 2; i <= cost.length; i++){dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);}return dp[cost.length];}
}

注意,此时到达数组顶部对应的是数组最后一个位置向后一个位置,即

总结

动态规划的初始条件: dp[0] = 0,dp[1] = 0, 反直觉的都是0,因为可以选择直接站在上面
动态规划的递推公式:f(i) = min(f(i-1)+nums[i-1] + f(i-2)+nums[i-2])
动态规划的终止条件:来到数组最后一个位置的下一个位置,虚终点

二.跳跃游戏相关专题

在实际的题目中,往往情况更为复杂,且限制较多,比如在大部分情况下,直接的动态规划、BFS和DFS都会超时或者所需空间太大,此时就要进行优化,通常进行剪枝就可以通过,但有时候需要两种方法的结合来进行优化。

1-3.  贪心/动态规划/dfs 相关

        leetcode55 跳跃游戏

        leetcode45 跳跃游戏 II

        leetcode1306 跳跃游戏 III

  见 跳跃游戏 (贪心/动态规划/dfs)

4.leetcode1696 跳跃游戏 VI

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。

一开始你在下标 0 处。每一步,你最多可以往前跳 k 步,但你不能跳出数组的边界。也就是说,你可以从下标 i 跳到 [i + 1, min(n - 1, i + k)] 包含 两个端点的任意位置。

你的目标是到达数组最后一个位置(下标为 n - 1 ),你的 得分 为经过的所有数字之和。

请你返回你能得到的 最大得分 。

输入:nums = [1,-1,-2,4,-7,3], k = 2
输出:7
解释:你可以选择子序列 [1,-1,4,3] (上面加粗的数字),和为 7 。

基础动态规划

class Solution {public int maxResult(int[] nums, int k) {int[] dp = new int[nums.length];dp[0] = nums[0];for(int i = 1; i < nums.length; i++){int max = Integer.MIN_VALUE;for(int j = 1; j <= k; j++){if(i - j >= 0){max = Math.max(max,dp[i-j]);dp[i] = max+nums[i];}}}return dp[nums.length-1];}
}

 相同思想的版本【1】

public int maxResult(int[] nums, int k) {int[] dp = new int[nums.length];Arrays.fill(dp, Integer.MIN_VALUE);dp[0] = nums[0];for (int i = 1; i < nums.length; i++) {for (int j = Math.max(0, i - k); j < i; j++) {dp[i] = Math.max(dp[i], dp[j]);}dp[i] += nums[i];}return dp[nums.length - 1];}

那么在最基础的动态规划中,对于来到第i个位置的得分,我们向前遍历,拿到在区间内最大的和,然后加上nums[i]就是当前位置的最大值,对于初始化dp中的每一个值可以先赋最小值,即

 总结

动态规划的初始条件: dp[0] = nums[0],走到第一个位置最大值就是数组的第一个元素
动态规划的递推公式:f(i) = max{f(i-1),f(i-2),...,f(i-k)}+nums[i]
动态规划的终止条件:来到数组最后一个位置的下一个位置

然而毫无疑问是超时的,此代码只能通过部分案例

动态规划剪枝【2】

class Solution {public int maxResult(int[] nums, int k) {int[] dp = new int[nums.length];Arrays.fill(dp,Integer.MIN_VALUE);dp[0] = nums[0];for(int i = 0; i < nums.length; i++){for(int j = 1; j <= k && i+j < nums.length; j++){int max = dp[i] + nums[i+j];if(max > dp[i+j]) dp[i+j] = max;if(dp[i+j] >= dp[i]) break;}}return dp[nums.length-1];}
}

在这个版本的代码中,需要稍微转变一下思维,刚刚是从后向前进行动态规划的递推(在内循环中),现在是从前向后的动态规划的递推(在内循环中),那么对于从i位置开始的递推,我们能影响的范围就是i+1...i+k,即

注意点: if(dp[i+j] >= dp[i]) break中的“=”是什么作用?

若没有=号,即下方代码,可以运行发现不能通过,原因就在于相等的时候

class Solution {public int maxResult(int[] nums, int k) {int[] dp = new int[nums.length];Arrays.fill(dp,Integer.MIN_VALUE);dp[0] = nums[0];for(int i = 0; i < nums.length; i++){for(int j = 1; j <= k && i+j < nums.length; j++){int max = dp[i] + nums[i+j];if(max > dp[i+j]) dp[i+j] = max;if(dp[i+j] > dp[i]) break;}}return dp[nums.length-1];}
}

在大量相等的情况下,后一个出现的值要比前一个更有用,或者说更能决定后续的值。

特别的【3】:

“当dp[j] > dp[i]时,dp[j+1] 到 dp[i+k] 必然已经不会受dp[i]的影响了,因为dp[j]比dp[i]是更优的跳跃选择。 dp[j] == dp[i]时,dp[j+1] 到 dp[i+k] 的值会在遍历到j时被dp[j]重新更新一次,所以dp[i]对后面的更新是无效的。 所以当 dp[j] >= dp[i]时 break”

动态规划+滑动窗口【1】

对滑动窗口不熟悉的也可以看看这篇

Java-数据结构-滑动窗口

对双端队列不太熟悉看这篇

Java-Deque和Queue的使用、辨析和实战案例

public int maxResult(int[] nums, int k) {int[] dp = new int[nums.length];dp[0] = nums[0];// 构造滑动窗口的索引所对应的队列,队首至队尾的索引依次增大,但对应dp数组中的值依次降低Deque<Integer> windowIndices = new LinkedList<>();for (int i = 1; i < nums.length; i++) {// 如果新的索引i所对应的元素dp[i - 1]大于队尾rear所对应的数组元素dp[rear],就循环弹出队尾,直到新的元素i - 1能够成为队尾// 因为dp[rear] < dp[i - 1]且rear < i - 1,只要窗口继续向右移,dp[rear]就一定会被dp[i - 1]压在下面,不会成为窗口最大元素while (!windowIndices.isEmpty() && dp[i - 1] >= dp[windowIndices.peekLast()]) {windowIndices.pollLast();}windowIndices.offerLast(i - 1);if (windowIndices.peekFirst() < i - k) {windowIndices.pollFirst();}dp[i] = dp[windowIndices.peekFirst()] + nums[i];}return dp[nums.length - 1];}

1.定义一数据结构,装的是滑动窗口内的最大值(最大值索引),即在第i个元素处,滑动窗口内装的是i-1,i-2,...,i-k的最大值

2.在滑动窗口内保存最大值(最大值索引)的同时,将次最大值(次最大索引)也保存进来

3.对于滑动窗口内(索引)的,有,且最大值就是,其他都是候补

4.向右移动元素i时,将和滑动窗内的索引对应的元素进行比较

(1)若 < ,则把放到队尾,此时满足,索引依次增大,对应值依次减小

(2)若  >= ,则把从队尾去掉,在相等时,比i-1更早离开华东窗口范围

(3)和,...,比较,直到

(4)队首元素front和i-k比较,若 front<i-k证明从front无法跳到i,即超出了华东窗口的范围

 以nums = [10,-5,-2,4,0,3], k = 3为例

5.leetcode1413 逐步求和得到正数的最小值

给你一个整数数组 nums 。你可以选定任意的 正数 startValue 作为初始值。

你需要从左到右遍历 nums 数组,并将 startValue 依次累加上 nums 数组中的值。

请你在确保累加和始终大于等于 1 的前提下,选出一个最小的 正数 作为 startValue 。

输入:nums = [-3,2,-3,4,2]
输出:5
解释:如果你选择 startValue = 4,在第三次累加时,和小于 1 。累加求和startValue = 4 | startValue = 5 | nums(4 -3 ) = 1  | (5 -3 ) = 2    |  -3(1 +2 ) = 3  | (2 +2 ) = 4    |   2(3 -3 ) = 0  | (4 -3 ) = 1    |  -3(0 +4 ) = 4  | (1 +4 ) = 5    |   4(4 +2 ) = 6  | (5 +2 ) = 7    |   2

动态规划

对于本题,可以转化为求一类最小值问题,即前缀和中出现的最小值,即在遍历整个数组时,可能出现的最小值,即

只要最小值能够满足要求,那么在其他位置上也可以满足要求,上述出现的sum(1)~sum(n)即为前缀和的概念。

class Solution {public int minStartValue(int[] nums) {int[] dp = new int[nums.length];int min = nums[0];dp[0] = nums[0];for(int i = 1; i < nums.length; i++){dp[i] += dp[i-1]+nums[i];if(dp[i] < min){min = dp[i];}}if(min > 0) return 1;return 1-min;}
}

动态规划空间优化

class Solution {public int minStartValue(int[] nums) {int min = 0;int pre = 0;for(int i = 0; i < nums.length; i++){pre += nums[i];if(pre <= min){min = pre;}}if(min > 0) return 1;return 1-min;}
}

 总结

动态规划的初始条件: dp[0] = nums[0],
动态规划的递推公式:f(i) = f(i-1)+nums[i]
动态规划的终止条件:来到数组最后一个位置
动态规划的空间优化:由于当前值只和nums[i]和上一个值有关,用两个变量保存交替前进即可

6.leetcode1871 跳跃游戏 VII

给你一个下标从 0 开始的二进制字符串 s 和两个整数 minJump 和 maxJump 。一开始,你在下标 0 处,且该位置的值一定为 '0' 。当同时满足如下条件时,你可以从下标 i 移动到下标 j 处:

i + minJump <= j <= min(i + maxJump, s.length - 1) 且
s[j] == '0'.
如果你可以到达 s 的下标 s.length - 1 处,请你返回 true ,否则返回 false 。

输入:s = "011010", minJump = 2, maxJump = 3
输出:true
解释:
第一步,从下标 0 移动到下标 3 。
第二步,从下标 3 移动到下标 5 。

动态规划

class Solution {public boolean canReach(String s, int minJump, int maxJump) {int n = s.length();boolean[] dp = new boolean[n];dp[0] = true;for (int i = minJump; i < n; i++){if (s.charAt(i) == '1')continue;int left = i - maxJump < 0 ? 0 : i - maxJump;int right = i - minJump;for (int j = left; j <= right; j++){if (dp[j]){dp[i] = true;break;}}}return dp[n - 1];}
}

在最基础的动态规划中,我们来到第i个位置,第i个位置能不能走到,取决于之前在i- maxJump~i- minJump中是否有一个位置j,这个位置j是可以走到的,那么可得知从j可以蹦到i,如果在i- maxJump~i- minJump范围内都是不可达的,那么证明第i个位置也是不可达的。

 总结

动态规划的初始条件: dp[0] = True,默认第一个元素都是‘0’,都可以走到
动态规划的递推公式:f(i) = f(i-minJump) || f(i-minJump-1) || ... || f(i-maxJump)
动态规划的终止条件:来到数组最后一个位置

上述动态规划的世家复杂度肯定是很高的,在java语言下,勉强可以通过。

动态规划+滑动窗口【4】

class Solution {public boolean canReach(String s, int minJump, int maxJump) {int cnt = 1;boolean[] dp = new boolean[s.length()];dp[0] = true;for(int i = minJump; i < s.length(); i++){if(s.charAt(i) == '0' && cnt > 0){dp[i] = true;}if(i >= maxJump && dp[i-maxJump]){cnt--;}if(dp[i-minJump+1]){cnt++;}}return dp[s.length()-1];}
}

(1)在滑动窗口里保存一个可达坐标的数量(本题的滑动窗口的长度固定,随坐标一起动)

(2)若当前坐标i的字符是“0”并且可达数量大于0证明当前坐标可达,是True

(3)在向右移的过程中,影响的因素只有两个,一个左旧边界,一个右新边界,即:

动态规划+前缀和【5】

*前缀和的思想可以参考上题leetcode1413 逐步求和得到正数的最小值

class Solution {public boolean canReach(String s, int minJump, int maxJump) {int n = s.length();boolean[] f = new boolean[n];int[] pre = new int[n];f[0] = true;for(int i = 0; i < minJump; i++){pre[i] = 1;}for(int i = minJump; i < n ; i++){int left = i-maxJump;int right = i-minJump;if(s.charAt(i) == '0'){int total = pre[right] - (left <= 0 ? 0:pre[left-1]);if(total != 0) f[i] = true;}if(f[i]){pre[i] = pre[i-1] + 1;} else{pre[i] = pre[i-1];}}return f[n-1];}
}

(1)此种解法和上中解法一致,前缀和和上题的cnt具有相同的作用,即保存在一定区间内符合要求的数量

(2)当且在前缀和不为0时,i位置才可达,可达即要更新,即在上个区间的基础上加一:pre[i] = pre[i-1] + 1

BFS剪枝【6】

class Solution {public boolean canReach(String s, int minJump, int maxJump) {Queue<Integer> q = new ArrayDeque<>();q.add(0); // position:当前跳跃开始的位置int position = 0;while (!q.isEmpty()) {int i = q.poll();int left = Math.max(i + minJump, position + 1);int right = Math.min(i + maxJump, s.length() - 1);for (int j = left; j <= right; j++) {if (s.charAt(j) == '0'){q.add(j);if (j == s.length() - 1) {return true; }}}// 更新下一次跳跃开始的位置position = i + maxJump;}return false;}
}

(1)BFS主体通用代码

class Solution {public boolean canReach(String s, int minJump, int maxJump) {//0.初始化队列Queue<Integer> q = new ArrayDeque<>();//1.添加首个元素q.add(0); //2.进循环体,通常是队列内不为空while (!q.isEmpty()) {//2.1取出首个元素(最先进去的那个)int i = q.poll();//2.2开始一系列操作,最主要的功能是为队列添加新的元素{******操作*******}}}
}

(2)循环提前终止条件

if (j == s.length() - 1) {return true;
}

(3)在主功能循环体内,在左右边界内,循环检查是否为"0",为0就进队列

for (int j = left; j <= right; j++) {if (s.charAt(j) == '0'){q.add(j);if (j == s.length() - 1) {return true; }}
}

(4)主体剪枝部分

int left = Math.max(i + minJump, position + 1);
//left左边界,由于序号递增,之前检查过的区间不需要再检查int right = Math.min(i + maxJump, s.length() - 1);
//right实际是队右边界的框定,无优化position = i + maxJump;
//position保存这次已经遍历过的位置

重点:

position保存这次已经遍历过的位置

左边界left,由于序号递增,之前检查过的区间不需要再检查

参考来源

【1】leetcode KaguyaShinomiya 跳跃游戏——从动态规划到滑动窗口

【2】leetcode MapleStore Java 动态规划+剪枝 7ms击败100%

【3】leetcode ives-nx Java 动态规划+剪枝 7ms击败100%

【4】leetcode 褴褛青衫 滑窗思想+dp---不需要前缀和---简单明了的O(n)/O(n)解法

【5】leetcode 官方 跳跃游戏 VII

【6】leetcode  gfu_ 跳跃游戏 VII

跳跃游戏 (动态规划剪枝/前缀和/滑动窗口/BFS剪枝)相关推荐

  1. 【LeetCode】LeetCode之跳跃游戏——动态规划+贪心算法

    [LeetCode]LeetCode之打家劫舍[暴力递归.动态规划.动态规划之优化空间的具体分析与实现] https://blog.csdn.net/Kevinnsm/article/details/ ...

  2. 七月集训(6)滑动窗口+动态规划

    文章目录 1.LeetCode:643. 子数组最大平均数 I 2.LeetCode:718. 最长重复子数组 3.LeetCode:978. 最长湍流子数组 4.LeetCode:1052. 爱生气 ...

  3. Leetcode1696. 跳跃游戏 VI[C++题解]:dp和单调队列求滑动窗口最值

    文章目录 题目分析 题目链接 单调队列板子链接 Deque知识补充 题目分析 题目重述:给定一个数组(有正数有负数)和一个步长k,从下标0处开始往前跳,每次最多往前跳k步.求跳到最后一个位置,得分之和 ...

  4. [Leetcode][第718题][JAVA][最长重复子数组][动态规划][滑动窗口][暴力]

    [问题描述][中等] 给两个整数数组 A 和 B ,返回两个数组中公共的.长度最长的子数组的长度.示例 1:输入: A: [1,2,3,2,1] B: [3,2,1,4,7] 输出: 3 解释: 长度 ...

  5. Leetcode1703. 得到连续 K 个 1 的最少相邻交换次数[C++题解]:难(货仓选址加强版+滑动窗口+前缀和)

    文章目录 题目分析 题目链接 题目分析 首先需要明确一点:最优结果中1的相对位置和开始时不会改变.否则的话就是交换两个1,会徒劳增加交换次数. 比如[1,0,0,0,0,0,1,1],最后变成[0,0 ...

  6. Longest X 贪心,滑动窗口,前缀和(400)

    题意 : 给一个x.串,每次操作可以将一个.换成一个x,问0-k次操作的过程中,连续的x的数量最多是多少 思路 : 转换条件,就是我们最多可以将k个.换成x 在所有满足.数小于等于k的区间中,最大的区 ...

  7. [Leedcode][JAVA][第209题][长度最小的子数组][滑动窗口][前缀和][二分查找][双指针]

    [问题描述][中等] 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度.如果不存在符合条件的连续子数组,返回 0.示例: 输入: ...

  8. AcWing 1236. 递增三元组 (flag + 前缀和 | 二分 | 滑动窗口)

    1236. 递增三元组 解题思路 最开始想到3重循环枚举三个数组,然后最内层用条件语句判断一下即可,但是数据范围为10510^5105,三重循环肯定会超时 那么这道题很可能需要的算法复杂度为O(n)O ...

  9. 跳跃游戏 (贪心/动态规划/dfs)

    1.跳跃游戏简单介绍 跳跃游戏是一种典型的算法题目,经常是给定一数组arr[],从数组的某一位置i出发,根据一定的跳跃规则,比如从i位置能跳arr[i]步,或者小于arr[i]步,或者固定步数,直到到 ...

最新文章

  1. 编程珠玑第四章习题答案
  2. Mac没有winnt格式_8款优秀软件,让你使用mac更舒适
  3. Keil C51软件的使用教程
  4. HALCON示例程序classify_image_class_knn.hdev使用KNN分类器对多通道图像进行分割
  5. ios framework 调用第三方 framework_Python基础:标准库和常用的第三方库
  6. 配置linux登录超时命令,LINUX中 设置登录超时
  7. 接口测试并不只是测试参数和返回值
  8. Powershell进阶学习(6) 部署 Windows PowerShell Web 访问
  9. Oracle Cluster verification utility failed 的解决方法
  10. 【hihocoder 1032】最长回文子串
  11. 核心JavaScript(一):数据类型与变量之Number再探
  12. 玩玩Linux云主机-安装MySQL ,The server quit without updating PID file,Linux chown 权限管理
  13. 用css和js分别实现三级导航菜单
  14. 3、一层、二层、交换机原理、Cisco软件及命令
  15. 黑产用“未来武器”破解验证码
  16. A short theory of channel flow
  17. 一元享移动怎么样_移动新套餐:18元享“全免流”+1元1G流量,阿里鱼卡要遭殃?...
  18. 安卓紧急警报_我们的紧急警报系统依赖于重叠的私有服务混乱局面
  19. 为什么说信息是负熵?
  20. 【经典】一个大数据学习的解决方案

热门文章

  1. 77、自动喷水灭火系统的巡查内容
  2. CAD如何修改标注样式?
  3. java 调excel 的宏_Microsoft Excel宏来运行Java程序
  4. MySQL数据库框架
  5. 名帖291 董其昌 行书《乐志论》
  6. bootstrap切换tab页局部刷新_Rails 用 RJS 简单有效的实现页面局部刷新
  7. i511320h和锐龙r75800h性能 r7 5800h和 i5 11320h 评测
  8. 前端历程(一)------初识前端
  9. csp怎么给线条描边,插画师要失业了?还在纠结阴影怎么画?CSP软件能直接自动生成...
  10. 基于STM32的有限词条语音识别与对话模块