LeetCode 1871. 跳跃游戏 VII(中等)

题目描述

给你一个下标从 0 开始的二进制字符串 s 和两个整数 minJumpmaxJump 。一开始,你在下标 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),需要队列和访问标记数组。
相关推荐
橙留香mostarrain1 小时前
从零开始的数据结构教程(四) 图论基础与算法实战
数据结构·算法·图论
xy_optics1 小时前
Wirtinger Flow算法的matlab实现和python实现
python·算法·matlab
zyq~1 小时前
【课堂笔记】EM算法
人工智能·笔记·算法·机器学习·概率论·gmm·em算法
Giser探索家2 小时前
「卫星百科」“绿色守卫”高分六号
大数据·人工智能·数码相机·算法·分类·云计算
思绪漂移2 小时前
线性回归中标准方程法求逆失败的解法:正则化
人工智能·算法·回归·线性回归
JK0x072 小时前
代码随想录算法训练营 Day59 图论Ⅸ dijkstra优化版 bellman_ford
算法·图论
白熊1883 小时前
【机器学习基础】机器学习入门核心算法:随机森林(Random Forest)
算法·随机森林·机器学习
宇钶宇夕3 小时前
SCL语言两台电机正反转控制程序从选型、安装到调试全过程的详细步骤指南(下)
运维·程序人生·算法·自动化
tt5555555555553 小时前
每日一题——提取服务器物料型号并统计出现次数
数据结构·c++·算法
linux-hzh3 小时前
day01
java·mysql·算法·leetcode