题目描述
给你一个下标从 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
。
示例 1:
输入:s = "011010", minJump = 2, maxJump = 3
输出:true
解释:
第一步,从下标 0 移动到下标 3 。
第二步,从下标 3 移动到下标 5 。
示例 2:
输入:s = "01101110", minJump = 2, maxJump = 3
输出:false
提示:
2 <= s.length <= 10^5
s[i]
要么是'0'
,要么是'1'
s[0] == '0'
1 <= minJump <= maxJump < s.length
思路分析
这道题可以用多种方法解决,我们来分析几种主要的解法:
方法一:动态规划 + 前缀和优化
核心思想:
- 定义 dp[i] 表示是否能从起点到达位置 i
- 对于位置 i,如果 s[i] == '0',那么需要检查是否存在某个位置 j,使得:
- dp[j] == true(位置 j 可达)
- j + minJump <= i <= j + maxJump(从 j 可以跳到 i)
优化关键:
使用前缀和来快速判断区间内是否存在可达的位置,避免重复遍历。
方法二:BFS(广度优先搜索)
核心思想:
- 将问题抽象为图的连通性问题
- 每个值为 '0' 的位置是图中的节点
- 如果位置 i 可以跳到位置 j,则在它们之间连边
- 使用 BFS 判断起点和终点是否连通
算法过程
通过示例 s = "011010", minJump = 2, maxJump = 3 来详细解释 动态规划 + 前缀和优化 这个算法。
第一步:理解问题转换
原问题: 从位置 i 能跳到哪些位置?
- 从位置 i 可以跳到 [i + minJump, i + maxJump] 范围内值为 '0' 的位置
转换后: 哪些位置能跳到位置 j?
- 能跳到位置 j 的位置范围是 [j - maxJump, j - minJump]
cs
原始字符串: s = "011010"
索引: 0 1 2 3 4 5
minJump = 2, maxJump = 3
第二步:初始化状态
cs
dp[i] 表示是否能到达位置 i
prefixSum[i] 表示 dp[0] 到 dp[i] 中 true 的个数
初始状态:
位置: 0 1 2 3 4 5
字符: 0 1 1 0 1 0
dp: [T, F, F, F, F, F] (只有起点可达)
前缀和: [1, 1, 1, 1, 1, 1] (初始时只有 dp[0] = true)
第三步:逐步计算每个位置
计算位置 1 (i = 1)
cs
s[1] = '1' → 无法到达,跳过
dp[1] = false
prefixSum[1] = prefixSum[0] + 0 = 1
计算位置 2 (i = 2)
cs
s[2] = '1' → 无法到达,跳过
dp[2] = false
prefixSum[2] = prefixSum[1] + 0 = 1
计算位置 3 (i = 3) - 关键步骤
cs
s[3] = '0' → 可能到达,需要检查
能跳到位置 3 的范围计算:
left = max(0, 3 - 3) = max(0, 0) = 0
right = max(0, 3 - 2) = max(0, 1) = 1
检查区间 [0, 1] 内是否有可达位置:
sum = prefixSum[1] - prefixSum[-1] = 1 - 0 = 1 > 0
所以 dp[3] = true
prefixSum[3] = prefixSum[2] + 1 = 1 + 1 = 2
位置 3 的计算过程:
cs
位置: 0 1 2 3 4 5
字符: 0 1 1 0 1 0
dp: [T, F, F, ?, F, F]
↑ ↑
| |
检查范围 [0,1]
从位置 0 能跳到位置 3 吗?
0 + minJump = 0 + 2 = 2 ≤ 3 ≤ 0 + 3 = 3 ✓
且 s[3] = '0' ✓
所以位置 3 可达!
结果: dp[3] = true
计算位置 4 (i = 4)
cs
s[4] = '1' → 无法到达,跳过
dp[4] = false
prefixSum[4] = prefixSum[3] + 0 = 2
计算位置 5 (i = 5) - 最终目标
cs
s[5] = '0' → 可能到达,需要检查
能跳到位置 5 的范围计算:
left = max(0, 5 - 3) = max(0, 2) = 2
right = max(0, 5 - 2) = max(0, 3) = 3
检查区间 [2, 3] 内是否有可达位置:
sum = prefixSum[3] - prefixSum[1] = 2 - 1 = 1 > 0
所以 dp[5] = true
prefixSum[5] = prefixSum[4] + 1 = 2 + 1 = 3
位置 5 的计算过程:
cs
位置: 0 1 2 3 4 5
字符: 0 1 1 0 1 0
dp: [T, F, F, T, F, ?]
↑ ↑
| |
检查范围 [2,3]
从位置 3 能跳到位置 5 吗?
3 + minJump = 3 + 2 = 5 ≤ 5 ≤ 3 + 3 = 6 ✓
且 s[5] = '0' ✓
所以位置 5 可达!
结果: dp[5] = true
第四步:完整的状态转移过程
cs
步骤 | 位置 | 字符 | 检查范围 | 前缀和查询 | dp值 | 前缀和数组
-----|------|------|----------|------------|---------------|------------
初始 | - | - | - | - | [T,F,F,F,F,F] | [1,1,1,1,1,1]
1 | 1 | 1 | - | - | [T,F,F,F,F,F] | [1,1,1,1,1,1]
2 | 2 | 1 | - | - | [T,F,F,F,F,F] | [1,1,1,1,1,1]
3 | 3 | 0 | [0,1] | 1-0=1 | [T,F,F,T,F,F] | [1,1,1,2,2,2]
4 | 4 | 1 | - | - | [T,F,F,T,F,F] | [1,1,1,2,2,2]
5 | 5 | 0 | [2,3] | 2-1=1 | [T,F,F,T,F,T] | [1,1,1,2,2,3]
第五步:前缀和优化的关键
为什么需要前缀和?
如果不用前缀和,我们需要这样检查:
cs
// 朴素方法 - O(n²) 时间复杂度
boolean canReach = false;
for (int j = left; j <= right; j++) {
if (dp[j]) {
canReach = true;
break;
}
}
使用前缀和优化后:
cs
// 优化方法 - O(1) 时间复杂度
int sum = prefixSum[right] - (left > 0 ? prefixSum[left-1] : 0);
boolean canReach = sum > 0;
Java 解法
解法一:动态规划 + 前缀和
java
public class Solution {
public boolean canReach(String s, int minJump, int maxJump) {
int n = s.length();
// 如果终点是 '1',直接返回 false
if (s.charAt(n - 1) == '1') {
return false;
}
// dp[i] 表示是否能到达位置 i
boolean[] dp = new boolean[n];
dp[0] = true;
// 前缀和数组,prefixSum[i] 表示 dp[0] 到 dp[i] 中 true 的个数
int[] prefixSum = new int[n];
prefixSum[0] = 1; // dp[0] = true,所以前缀和为 1
for (int i = 1; i < n; i++) {
// 只有当前位置是 '0' 时才可能到达
if (s.charAt(i) == '0') {
// 计算能跳到位置 i 的范围 [left, right]
int left = Math.max(0, i - maxJump);
int right = i - minJump;
// 确保范围有效
if (right >= 0 && left <= right) {
// 使用前缀和快速查询范围内可达位置的数量
int count = prefixSum[right] - (left > 0 ? prefixSum[left - 1] : 0);
dp[i] = count > 0;
}
}
// 更新前缀和
prefixSum[i] = prefixSum[i - 1] + (dp[i] ? 1 : 0);
}
return dp[n - 1];
}
}
解法二:BFS
java
import java.util.*;
public class Solution {
public boolean canReach(String s, int minJump, int maxJump) {
int n = s.length();
// 如果终点是 '1',直接返回 false
if (s.charAt(n - 1) == '1') {
return false;
}
Queue<Integer> queue = new LinkedList<>();
boolean[] visited = new boolean[n];
queue.offer(0);
visited[0] = true;
// 记录上次检查的最远位置,避免重复检查
int farthest = 0;
while (!queue.isEmpty()) {
int curr = queue.poll();
// 如果到达终点,返回 true
if (curr == n - 1) {
return true;
}
// 计算可跳跃的范围
int start = Math.max(farthest + 1, curr + minJump);
int end = Math.min(n - 1, curr + maxJump);
// 遍历可跳跃的位置
for (int next = start; next <= end; next++) {
if (s.charAt(next) == '0' && !visited[next]) {
visited[next] = true;
queue.offer(next);
}
}
// 更新最远检查位置
farthest = Math.max(farthest, end);
}
return false;
}
}
C# 解法
解法一:动态规划 + 前缀和
cs
public class Solution {
public bool CanReach(string s, int minJump, int maxJump) {
int n = s.Length;
// 如果终点是 '1',直接返回 false
if (s[n - 1] == '1') {
return false;
}
// dp[i] 表示是否能到达位置 i
bool[] dp = new bool[n];
dp[0] = true;
// 前缀和数组,prefixSum[i] 表示 dp[0] 到 dp[i] 中 true 的个数
int[] prefixSum = new int[n];
prefixSum[0] = 1; // dp[0] = true,所以前缀和为 1
for (int i = 1; i < n; i++) {
// 只有当前位置是 '0' 时才可能到达
if (s[i] == '0') {
// 计算能跳到位置 i 的范围 [left, right]
int left = Math.Max(0, i - maxJump);
int right = i - minJump;
// 确保范围有效
if (right >= 0 && left <= right) {
// 使用前缀和快速查询范围内可达位置的数量
int count = prefixSum[right] - (left > 0 ? prefixSum[left - 1] : 0);
dp[i] = count > 0;
}
}
// 更新前缀和
prefixSum[i] = prefixSum[i - 1] + (dp[i] ? 1 : 0);
}
return dp[n - 1];
}
}
解法二:BFS
cs
using System;
using System.Collections.Generic;
public class Solution {
public bool CanReach(string s, int minJump, int maxJump) {
int n = s.Length;
// 如果终点是 '1',直接返回 false
if (s[n - 1] == '1') {
return false;
}
Queue<int> queue = new Queue<int>();
bool[] visited = new bool[n];
queue.Enqueue(0);
visited[0] = true;
// 记录上次检查的最远位置,避免重复检查
int farthest = 0;
while (queue.Count > 0) {
int curr = queue.Dequeue();
// 如果到达终点,返回 true
if (curr == n - 1) {
return true;
}
// 计算可跳跃的范围
int start = Math.Max(farthest + 1, curr + minJump);
int end = Math.Min(n - 1, curr + maxJump);
// 遍历可跳跃的位置
for (int next = start; next <= end; next++) {
if (s[next] == '0' && !visited[next]) {
visited[next] = true;
queue.Enqueue(next);
}
}
// 更新最远检查位置
farthest = Math.Max(farthest, end);
}
return false;
}
}
复杂度分析
动态规划 + 前缀和方法:
- 时间复杂度: O(n),其中 n 是字符串长度。每个位置只访问一次。
- 空间复杂度: O(n),需要 dp 数组和前缀和数组。
BFS 方法:
- 时间复杂度: O(n),通过 farthest 指针避免重复访问,每个位置最多访问一次。
- 空间复杂度: O(n),需要队列和访问标记数组。