0403-青蛙过河

Raphael Liu Lv10

一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。

给你石子的位置列表 stones(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时,
青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃 1 个单位(即只能从单元格 1 跳至单元格 2 )。

如果青蛙上一步跳跃了 k _ _ 个单位,那么它接下来的跳跃距离只能选择为 k - 1k _ _ 或 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 <= 2000
  • 0 <= stones[i] <= 231 - 1
  • stones[0] == 0
  • stones 按严格升序排列

方法一:记忆化搜索 + 二分查找

思路及算法

最直接的想法是使用深度优先搜索的方式尝试所有跳跃方案,直到我们找到一组可行解为止。但是不加优化的该算法的时间复杂度在最坏情况下是指数级的,因此考虑优化。

注意到当青蛙每次能够跳跃的距离仅取决于青蛙的「上一次跳跃距离」。而青蛙此后能否到达终点,只和它「现在所处的石子编号」以及「上一次跳跃距离」有关。因此我们可以将这两个维度综合记录为一个状态。使用记忆化搜索的方式优化时间复杂度。

具体地,当青蛙位于第 $i$ 个石子,上次跳跃距离为 lastDis 时,它当前能够跳跃的距离范围为 $[\textit{lastDis}-1,\textit{lastDis}+1]$。我们需要分别判断这三个距离对应的三个位置是否存在石子。注意到给定的石子列表为升序,所以我们可以利用二分查找来优化查找石子的时间复杂度。每次我们找到了符合要求的位置,我们就尝试进行一次递归搜索即可。

为了优化编码,我们可以认为青蛙的初始状态为:「现在所处的石子编号」为 $0$(石子从 $0$ 开始编号),「上一次跳跃距离」为 $0$(这样可以保证青蛙的第一次跳跃距离为 $1$)。

代码

[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<unordered_map<int, int>> rec;

bool dfs(vector<int>& stones, int i, int lastDis) {
if (i == stones.size() - 1) {
return true;
}
if (rec[i].count(lastDis)) {
return rec[i][lastDis];
}
for (int curDis = lastDis - 1; curDis <= lastDis + 1; curDis++) {
if (curDis > 0) {
int j = lower_bound(stones.begin(), stones.end(), curDis + stones[i]) - stones.begin();
if (j != stones.size() && stones[j] == curDis + stones[i] && dfs(stones, j, curDis)) {
return rec[i][lastDis] = true;
}
}
}
return rec[i][lastDis] = false;
}

bool canCross(vector<int>& stones) {
int n = stones.size();
rec.resize(n);
return dfs(stones, 0, 0);
}
};
[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
private Boolean[][] rec;

public boolean canCross(int[] stones) {
int n = stones.length;
rec = new Boolean[n][n];
return dfs(stones, 0, 0);
}

private boolean dfs(int[] stones, int i, int lastDis) {
if (i == stones.length - 1) {
return true;
}
if (rec[i][lastDis] != null) {
return rec[i][lastDis];
}

for (int curDis = lastDis - 1; curDis <= lastDis + 1; curDis++) {
if (curDis > 0) {
int j = Arrays.binarySearch(stones, i + 1, stones.length, curDis + stones[i]);
if (j >= 0 && dfs(stones, j, curDis)) {
return rec[i][lastDis] = true;
}
}
}
return rec[i][lastDis] = false;
}
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var canCross = function(stones) {
const n = stones.length;
rec = new Array(n).fill(0).map(() => new Map());

const dfs = (stones, i, lastDis) => {
if (i === stones.length - 1) {
return true;
}
if (rec[i].has(lastDis)) {
return rec[i].get(lastDis);
}
for (let curDis = lastDis - 1; curDis <= lastDis + 1; curDis++) {
if (curDis > 0) {
const j = lower_bound(stones, curDis + stones[i]);
if (j !== stones.length && stones[j] === curDis + stones[i] && dfs(stones, j, curDis)) {
rec[i].set(lastDis, true);
return rec[i].get(lastDis);
}
}
}
rec[i].set(lastDis, false);
return rec[i].get(lastDis);
}

return dfs(stones, 0, 0);
};

function lower_bound(nums, target) {
let lo = 0, hi = nums.length;

while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);

if (nums[mid] >= target) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func canCross(stones []int) bool {
n := len(stones)
dp := make([]map[int]bool, n-1)
for i := range dp {
dp[i] = map[int]bool{}
}
var dfs func(int, int) bool
dfs = func(i, lastDis int) (res bool) {
if i == n-1 {
return true
}
if res, has := dp[i][lastDis]; has {
return res
}
defer func() { dp[i][lastDis] = res }()
for curDis := lastDis - 1; curDis <= lastDis+1; curDis++ {
if curDis > 0 {
j := sort.SearchInts(stones, curDis+stones[i])
if j != n && stones[j] == curDis+stones[i] && dfs(j, curDis) {
return true
}
}
}
return false
}
return dfs(0, 0)
}
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
struct HashTable {
int key, val;
UT_hash_handle hh;
};

struct HashTable** rec;

bool count(struct HashTable** hashTable, int ikey) {
struct HashTable* tmp;
HASH_FIND_INT(*hashTable, &ikey, tmp);
return tmp != NULL;
}

int modify(struct HashTable** hashTable, int ikey, int ival) {
struct HashTable* tmp;
HASH_FIND_INT(*hashTable, &ikey, tmp);
if (tmp == NULL) {
tmp = malloc(sizeof(struct HashTable));
tmp->key = ikey, tmp->val = ival;
HASH_ADD_INT(*hashTable, key, tmp);
} else {
tmp->val = ival;
}
return ival;
}

int query(struct HashTable** hashTable, int ikey) {
struct HashTable* tmp;
HASH_FIND_INT(*hashTable, &ikey, tmp);
return tmp->val;
}

int lower_bound(int* stones, int stonesSize, int target) {
int left = 0, right = stonesSize;
while (left < right) {
int mid = (left + right) >> 1;
if (stones[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}

bool dfs(int* stones, int stonesSize, int i, int lastDis) {
if (i == stonesSize - 1) {
return true;
}
if (count(&rec[i], lastDis)) {
return query(&rec[i], lastDis);
}
for (int curDis = lastDis - 1; curDis <= lastDis + 1; curDis++) {
if (curDis > 0) {
int j = lower_bound(stones, stonesSize, curDis + stones[i]);
if (j != stonesSize && stones[j] == curDis + stones[i] && dfs(stones, stonesSize, j, curDis)) {
return modify(&rec[i], lastDis, true);
}
}
}
return modify(&rec[i], lastDis, false);
}

bool canCross(int* stones, int stonesSize) {
rec = malloc(sizeof(struct HashTable*) * stonesSize);
memset(rec, 0, sizeof(struct HashTable*) * stonesSize);
return dfs(stones, stonesSize, 0, 0);
}

复杂度分析

  • 时间复杂度:$O(n^2\log n)$,其中 $n$ 是石子的数量。因为青蛙仅能在石子间跳跃,且不能向后方(起点的方向)跳跃,而第 $i$ 个石子后方只有 $i-1$ 个石子,因此在任意位置,青蛙的「上一次跳跃距离」至多只有 $O(n)$ 种,状态总数为 $O(n^2)$。最坏情况下我们要遍历每一个状态,每次我们需要 $O(\log n)$ 的时间查找指定位置的石子是否存在,相乘即可得到最终时间复杂度。

  • 空间复杂度:$O(n^2)$,其中 $n$ 是石子的数量。最坏情况下我们需要记录全部 $O(n^2)$ 个状态。

方法二:动态规划

思路及算法

我们也可以使用动态规划的方法,令 dp}[i][k]$ 表示青蛙能否达到「现在所处的石子编号」为 $i$ 且「上一次跳跃距离」为 $k$ 的状态。

这样我们可以写出状态转移方程:

$dp}[i][k] = \textit{dp}[j][k - 1] \bigvee \textit{dp}[j][k] \bigvee \textit{dp}[j][k + 1]$$

式中 $j$ 代表了青蛙的「上一次所在的石子编号」,满足 $stones[i] - stones[j] = k$。

和方法一相同,状态转移的初始条件为 dp}[0][0] = \text{true,表示:「现在所处的石子编号」为 $0$(石子从 $0$ 开始编号),「上一次跳跃距离」为 $0$(这样可以保证青蛙的第一次跳跃距离为 $1$)。当我们找到一个 dp}[n-1][k]$ 为真时,我们就知道青蛙可以到达终点(第 $n-1$ 个石子)。

具体地,对于第 $i$ 个石子,我们首先枚举所有的 $j$(即上一次所在的石子编号),那么「上一次跳跃距离」$k$ 即为 $stones[i] - stones[j]$。如果在第 $j$ 个石子上,青蛙的「上一次跳跃距离」可以为 $k-1,k,k+1$ 三者之一,那么我们此时的方案即为合法方案。因此我们只需要检查 dp}[j][k - 1], \textit{dp}[j][k], \textit{dp}[j][k + 1]$ 是否有至少一个为真即可。

优化

为了优化程序运行速度,我们还将推出两个结论,并做出优化:

  1. 「现在所处的石子编号」为 $i$ 时,「上一次跳跃距离」$k$ 必定满足 $k \leq i$。
    • 当青蛙位于第 $0$ 个石子上时,青蛙的上一次跳跃距离限定为 $0$,之后每次跳跃,青蛙所在的石子编号至少增加 $1$,而每次跳跃距离至多增加 $1$。
    • 跳跃 $m$ 次后,青蛙「现在所处的石子编号」$i \geq m$,「上一次跳跃距离」$k \leq m$,因此 $k\leq i$。
    • 这样我们可以将状态数约束在 $O(n^2)$。
    • 我们可以从后向前枚举「上一次所在的石子编号」$j$,当「上一次跳跃距离」$k$ 超过了 $j+1$ 时,我们即可以停止跳跃,因为在第 $j$ 个石子上我们至多只能跳出 $j+1$ 的距离。
  2. 当第 $i$ 个石子与第 $i-1$ 个石子距离超过 $i$ 时,青蛙必定无法到达终点。
    • 由结论 $1$ 可知,当青蛙到达第 $i-1$ 个石子时,它的「上一次跳跃距离」至多为 $i-1$,因此青蛙在第 $i$ 个石子上最远只能跳出 $i$ 的距离。
    • 而距离第 $i-1$ 个石子最近的石子即为第 $i$ 个石子,它们的距离超过了青蛙当前能跳出的最远距离,因此青蛙无路可跳。
    • 因此我们可以提前检查是否有相邻的石子不满足条件,如果有,我们可以提前返回 false。

代码

[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
bool canCross(vector<int>& stones) {
int n = stones.size();
vector<vector<int>> dp(n, vector<int>(n));
dp[0][0] = true;
for (int i = 1; i < n; ++i) {
if (stones[i] - stones[i - 1] > i) {
return false;
}
}
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;
}
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;
}
};
[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public boolean canCross(int[] stones) {
int n = stones.length;
boolean[][] dp = new boolean[n][n];
dp[0][0] = true;
for (int i = 1; i < n; ++i) {
if (stones[i] - stones[i - 1] > i) {
return false;
}
}
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;
}
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;
}
}
[sol2-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var canCross = function(stones) {
const n = stones.length;
const dp = new Array(n).fill(0).map(() => new Array(n).fill(0));
dp[0][0] = true;
for (let i = 1; i < n; ++i) {
if (stones[i] - stones[i - 1] > i) {
return false;
}
}
for (let i = 1; i < n; ++i) {
for (let j = i - 1; j >= 0; --j) {
const k = stones[i] - stones[j];
if (k > j + 1) {
break;
}
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;
};
[sol2-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func canCross(stones []int) bool {
n := len(stones)
dp := make([][]bool, n)
for i := range dp {
dp[i] = make([]bool, n)
}
dp[0][0] = true
for i := 1; i < n; i++ {
if stones[i]-stones[i-1] > i {
return false
}
}
for i := 1; i < n; i++ {
for j := i - 1; j >= 0; j-- {
k := stones[i] - stones[j]
if k > j+1 {
break
}
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
}
[sol2-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool canCross(int* stones, int stonesSize) {
int dp[stonesSize][stonesSize];
memset(dp, 0, sizeof(dp));
dp[0][0] = true;
for (int i = 1; i < stonesSize; ++i) {
if (stones[i] - stones[i - 1] > i) {
return false;
}
}
for (int i = 1; i < stonesSize; ++i) {
for (int j = i - 1; j >= 0; --j) {
int k = stones[i] - stones[j];
if (k > j + 1) {
break;
}
dp[i][k] = dp[j][k - 1] || dp[j][k] || dp[j][k + 1];
if (i == stonesSize - 1 && dp[i][k]) {
return true;
}
}
}
return false;
}

复杂度分析

  • 时间复杂度:$O(n^2)$,其中 $n$ 是石子的数量。因为青蛙仅能在石子间跳跃,且不能向后方(起点的方向)跳跃,而第 $i$ 个石子后方只有 $i-1$ 个石子,因此在任意位置,青蛙的「上一次跳跃距离」至多只有 $n$ 种,状态总数为 $n^2$。最坏情况下我们要遍历每一个状态,每次我们只需要 $O(1)$ 的时间计算当前状态是否可达,相乘即可得到最终时间复杂度。

  • 空间复杂度:$O(n^2)$,其中 $n$ 是石子的数量。我们需要记录全部 $n^2$ 个状态。

 Comments
On this page
0403-青蛙过河