介绍
标签:动态规划
403. 青蛙过河
难度 困难
403. 青蛙过河:
https://leetcode-cn.com/problems/frog-jump
题目
一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。
给你石子的位置列表 stones(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。
开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格 1 跳至单元格 2 )。
如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1 个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
示例 1:
输入:stones = [0,1,3,5,6,8,12,17]
输出:true
解释:青蛙可以成功过河,按照如下方案跳跃:跳 1 个单位到第 2 块石子, 然后跳 2 个单位到第 3 块石子, 接着 跳 2 个单位到第 4 块石子, 然后跳 3 个单位到第 6 块石子, 跳 4 个单位到第 7 块石子, 最后,跳 5 个单位到第 8 个石子(即最后一块石子)。
示例 2:
输入:stones = [0,1,2,3,4,8,9,11]
输出:false
解释:这是因为第 5 和第 6 个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。
提示:
2 <= stones.length <= 20000 <= stones[i] <= 231 - 1stones[0] == 0
首先理解题目的意思
河里面有很多石子,青蛙要从坐左边跳到最右边
- 石子是升序
- 青蛙不能往回跳
- 青蛙可以间隔着跳
- 默认是在第一块石头上,而且第一次跳只能跳1个单位距离
最重要的是:
如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1 个单位
也就是说,他前一步跳的3,后面只能跳[2,3,4],如果在这个范围里面找不到了落脚点,那就掉水里了
动态规划
首先明确,下一步可以由三种状态转换而来
- 上一个石头到当前位置的距离为 k
- 上一个石头到当前位置的距离为 k+1
- 上一个石头到当前位置的距离为 k-1
在这个时候就可以回头去考虑在上一个石头的状态要怎么来,然后快进到动态规划
- 创建一个动归数组
dp[i][k],其中:- 行表示对应石子的编号
- 列表示上一跳的距离
- 初始化:
dp[0][0] = true,在第一个石头上的时候必然为true - 遍历每个石子,然后回头再去遍历已经遍历过的石子
- 根据上一次所在的石子位置,判断在[±1]的范围里面是否能够满足从上一个位置跳到当前位置
- 动归公式:
dp[i][k] = dp[j][k - 1] || dp[j][k] || dp[j][k + 1]
- 优化:相邻石子的距离大于其编号的时候,必然跳不过去(存在一个距离递增的关系)
- 最好的状态就是第一次跳1单位距离,第二次跳2单位距离…第n次跳n单位距离
- 其中的距离是永远无法超过其对应石子下标的
重点解释递归公式:dp[i][k] = dp[j][k - 1] || dp[j][k] || dp[j][k + 1]
- 优先要理解其中对应的意思
- i:当前石子的下标
- j:上一个石子的下标,范围必然是小于1的
- k:step的长度,只能对其进行±1操作
- 在当前石子前面的每一个石子都有机会调到当前石子来,所以全部遍历
- 判断是否存在
- 在上上个石子,只跳
k就能到上个石子 - 在上上个石子,只跳
k + 1就能到上个石子 - 在上上个石子,只跳
k - 1就能到上个石子 - 只要其中存在一个,那么就说明能用上一个石子跳到当前石子
- 即其中
||的作用
- 在上上个石子,只跳
宫老师说:
本质是利用「路径可逆」的性质,将问题进行了「等效对偶」
表面上我们在「正向递推」,但事实上我们是在验证是否存在某条「反向路径」到达位置 1
代码
class Solution {
public boolean canCross(int[] stones) {
int n = stones.length;
// 动归数组,其中行表示对应石子的编号,列表示上一跳的距离
boolean[][] dp = new boolean[n][n];
// 初始状态一定是为true
dp[0][0] = true;
for (int i = 1; i < n; ++i) {
for (int j = i - 1; j >= 0; --j) {
int k = stones[i] - stones[j];
// 相邻石子的距离大于其编号的时候,必然跳不过去(存在一个距离递增的关系)
if (k > j + 1) {
break;
}
// j对应的上一次所在的石子位置,判断在[k±1]的范围里面是否能够满足从j跳到i
dp[i][k] = dp[j][k - 1] || dp[j][k] || dp[j][k + 1];
// 到了数组最末尾的时候,进行一个判断,判断是否能跳到最后一个石子
if (i == n - 1 && dp[i][k]) {
return true;
}
}
}
return false;
}
}

dfs
是超时递归,好耶
dfs(int[] stones, int index, int step)
stones:石子列表,不变的
index:当前所在的石子的下标
step:上一次跳的长度
- 首先考虑递归出口
- 发现已经到不了,false
- 发现已经到了,true
- 分配的距离为0,也就是原地踏步,直接return
- 因为距离是可以±1的
- 所以可以向下进行三个递归搜索
- dfs(stones, i, step + 1);
- dfs(stones, i, step);
- dfs(stones, i, step - 1);
- 所以可以向下进行三个递归搜索
- 优化
- 首先是能够进行一个预先判断
- 两个相邻石子的距离大于其编号的时候,必然跳不过去(存在一个距离递增的关系)
- 剪枝,根据上一调的距离求出这一跳的范围
- 前面的距离短了,不用跳
- 后面的已经跳不到了,没有必要再看了
- 首先是能够进行一个预先判断
- 最后,反正超时了,看看就行了
代码
class Solution {
boolean found;
public boolean canCross(int[] stones) {
// 进行一个预先处理
for(int i = 3; i < stones.length; i++){
// 两个相邻石子的距离大于其编号的时候,必然跳不过去(存在一个距离递增的关系)
if(stones[i] - stones[i - 1] > i){
return false;
}
}
// 进行一个dfs,开始的时候只能从0开始跳一步
dfs(stones, 0, 1);
return found;
}
/**
* dfs
* @param stones 石子列表【不变】
* @param index 当前所在的石子的下标
* @param step 上一次跳的长度
*/
private void dfs(int[] stones, int index, int step){
// 发现没有距离或者已经到不了了
if(step == 0 || found) return;
// 到达最末尾
if(index == stones.length - 1){
found = true;
return;
}
// 能够跳到的距离
int reach = stones[index] + step;
// 从当前到最后,选个跳
for(int i = index + 1; i < stones.length; i++){
// 下面两个if相当于剪枝
// 前面的距离短了,不用跳
if(reach > stones[i]) continue;
// 后面的已经跳不到了,没有必要再看了
if(reach < stones[i]) break;
// [k±1]
dfs(stones, i, step + 1);
dfs(stones, i, step);
dfs(stones, i, step - 1);
}
}
}

致谢题解参考
1. 官方题解:青蛙过河
2. 宫水三叶老师的:一题四解 : 降低确定「记忆化容器大小」的思维难度 & 利用「对偶性质」构造有效状态值
本文解析了LeetCode 403青蛙过河问题,通过动态规划和深度优先搜索方法探讨青蛙能否成功过河。讲解了问题背景,动态规划的思路,以及如何利用递推公式和DFS剪枝优化。
&spm=1001.2101.3001.5002&articleId=116264745&d=1&t=3&u=e7468160c5c6491a82c6374d491b0d37)
1745

被折叠的 条评论
为什么被折叠?



