1. 题目来源
链接:403. 青蛙过河
2. 题目解析
很不错的一道 dp
问题,但是普通的 二维 dp
在此会 TLE
,故采用记忆化搜索来优化常数。也是一道很不错的记忆化搜索练习题目!
思路:
- 状态表示:
f[i][j]
表示跳到第i
个点, 且从第i
个点往后跳的长度可以为j
的所有方案的集合。f[i][j] = ture/false
。在该种状态表示下,答案即为f[n-1][0]~f[n-1][n]
中只要有一个为true
则为true
,否则为false
。 - 状态转移:
f[i][j]
下,上一个跳过来的步长可以为j-1、j、j+1
三种,当这三种中的任意一种能够之前的路径能够成立,那么就能保证f[i][j]=true
,则完成状态转移。考虑长度为j-1
的这个情况。即要跳到i
点,且长度是j-1
,则意味着上一个点一定是f[i-(j-1)][j-1]
这个情况转移来的,那么需要保证f[i-(j-1)][j-1]=true
且保证i-(j-1)
这个位置是石头。当这两个情况同时满足时,f[i][j]
即为true
。 - 初始化:
f[0][1]=true
,因为规定第一个石头只能跳一步,则位于第 0 号石头,且可以往后跳一步的方案是合法的,故为true
。 - 优化:哈希表将坐标映射到节点编号,用以快速判断上个点是否为石头。状态转移是
O(1)
的,状态个数是O(n*n)
的,最坏情况下依旧是 O ( n 2 ) O(n^2) O(n2) 的,但是由于中间过程中剪枝的存在可以优化一些常数,会比循环快一点。
dp
有两种实现方式,循环、记忆化搜索。这两种可以互相转换,都可以实现 dp
。且记忆化搜索更为简单,代码量更少,思维形式更符合 dp
的转移方程。
官方题解和下面的宽搜记忆化都很不凑。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n 2 ) O(n^2) O(n2)
代码:
详细注释:
const int N = 2005;
int f[N][N];
class Solution {
public:
vector<int> p;
unordered_map<int, int> h;
int dp(int i, int j) {
if (f[i][j] != -1) return f[i][j]; // 被搜过了,就直接返回当前值即可
f[i][j] = 0; // 赋初始值为 0,表示搜索过了,当前不可达
for (int l = max(1, j - 1); l <= j + 1; l ++ ) { // 最少跳一步,最多跳 j+1 步
int x = p[i] - l; // 计算 i 这个石头的上个石头的下标
if (h.count(x)) { // 判断上个石头是否存在
int k = h[x]; // 如果上个石头存在,得到上个石头的下标
if (dp(k, l)) { // 判断上个石头状态是否为 true,在此其跳出的距离一定是 l
f[i][j] = 1; // 为 true,则表示当前 i 石头跳出为 j 步的状态是可行的
break;
}
}
}
return f[i][j];
}
// f[i][j] 表示跳到了 i 这个点上,且从 i 这个点往后跳 j 步的所有方案的集合
bool canCross(vector<int>& stones) {
p = stones;
int n = stones.size();
for (int i = 0; i < n; i ++ ) h[stones[i]] = i;
memset(f, -1, sizeof f); // -1 作为无效值,表示状态未被更新,0 表示状态更新了,但是不能跳过来,1 表示能跳过来
f[0][1] = 1; // 初始化只能跳一步
for (int i = 0; i < n; i ++ ) // f[n-1][j:0~n] 只要有一个是能跳过来的,即状态为 1 就表示 n 号点能达
if (dp(n - 1, i)) // 记忆化搜索
return true;
return false;
}
};
期间看到一个记忆化宽搜 bfs
的代码感觉也很不错,实际上和本题的记忆化搜索思路上是一致的。感觉一个是从后向前、一个是从前向后。题解链接。
class Solution {
public:
bool canCross(vector<int>& stones) {
int n = stones.size();
unordered_map<int, int> hmp;
for (int i = 0; i < n; i ++ ) hmp[stones[i]] = i;
vector<vector<bool>> f(n, vector<bool>(n + 1, false));
queue<pair<int, int>> q;
q.push(make_pair(0, 0));
while (q.size()) {
auto t = q.front();
q.pop();
int cur_x = t.first, cur_k = t.second;
if (cur_x == n - 1) return true;
for (int i = - 1; i <= 1; i ++ ) {
if (cur_k + i <= 0) continue;
int next_k = cur_k + i;
int next_p = stones[cur_x] + next_k;
if (hmp.count(next_p)) {
int next_x = hmp[next_p];
if (!f[next_x][next_k]) {
f[next_x][next_k] = true;
q.push(make_pair(next_x, next_k));
}
}
}
}
return false;
}
};