游戏背景与问题建模
跳皮筋是中国传统的儿童游戏,玩家需要在皮筋形成的网格中跳跃前进,规则要求每次跳跃必须踩中特定节点,且不能踩到禁止区域。本文将其抽象为跳跃游戏问题(Jump Game):给定一个一维数组,每个元素表示从该位置最多可以向前跳跃的步数,判断能否到达终点;若能,求出最少跳跃次数。
该问题是 LeetCode 经典题型,同时也可扩展到二维网格上的最短路径问题。
算法思路
最少跳跃次数:动态规划
设 dp[i] 表示到达位置 i 所需的最少跳跃次数。状态转移方程为:
dp[i] = min(dp[j] + 1) 对所有满足 j + nums[j] >= i 的 j < i
贪心优化:O(n) 解法
维护当前能到达的最远边界(currentEnd)和下一跳能到达的最远边界(farthest)。当遍历到 currentEnd 时,跳跃次数加 1,并将 currentEnd 更新为 farthest。
二维扩展:网格最短路径
在二维网格中,每个格子有最大跳跃距离限制,使用 BFS 配合访问标记求解最短路径。
Java 完整实现
import java.util.*;
/**
* 跳皮筋:跳跃游戏与动态规划
* 包含一维最少跳跃、可达性判断、二维网格扩展
*/
public class JumpRope {
/**
* 动态规划求解最少跳跃次数
* @param nums 每个位置的最大跳跃步数
* @return 到达终点的最少跳跃次数,若无法到达返回 -1
*/
public int minJumpsDP(int[] nums) {
int n = nums.length;
if (n <= 1) return 0;
// dp[i] 表示到达位置 i 的最少跳跃次数
int[] dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
// 从 j 能否跳到 i
if (j + nums[j] >= i && dp[j] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[j] + 1);
}
}
}
return dp[n - 1] == Integer.MAX_VALUE ? -1 : dp[n - 1];
}
/**
* 贪心算法求解最少跳跃次数 O(n)
* @param nums 每个位置的最大跳跃步数
* @return 最少跳跃次数
*/
public int minJumpsGreedy(int[] nums) {
int n = nums.length;
if (n <= 1) return 0;
int jumps = 0; // 当前已用跳跃次数
int currentEnd = 0; // 当前这跳能到达的最远边界
int farthest = 0; // 下一跳能到达的最远边界
for (int i = 0; i < n - 1; i++) {
// 更新下一跳能到达的最远位置
farthest = Math.max(farthest, i + nums[i]);
// 到达当前跳的边界,必须使用下一跳
if (i == currentEnd) {
jumps++;
currentEnd = farthest;
// 若边界无法前进,则无法到达终点
if (currentEnd <= i) {
return -1;
}
}
}
return jumps;
}
/**
* 判断是否能到达终点
* @param nums 每个位置的最大跳跃步数
* @return 是否可达
*/
public boolean canJump(int[] nums) {
int n = nums.length;
int maxReach = 0;
for (int i = 0; i < n; i++) {
if (i > maxReach) {
return false; // 当前位置不可达
}
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= n - 1) {
return true;
}
}
return true;
}
/**
* 二维网格跳皮筋:从左上角到右下角的最短路径
* grid[i][j] 表示在该位置最多可以向上下左右各跳多少格
*/
public int minJumps2D(int[][] grid) {
int rows = grid.length;
if (rows == 0) return -1;
int cols = grid[0].length;
if (cols == 0) return -1;
// BFS 求最短路径
boolean[][] visited = new boolean[rows][cols];
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{0, 0, 0}); // {row, col, steps}
visited[0][0] = true;
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // 上下左右
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int r = curr[0], c = curr[1], steps = curr[2];
if (r == rows - 1 && c == cols - 1) {
return steps;
}
int maxStep = grid[r][c];
for (int[] dir : directions) {
// 向该方向尝试跳跃 1 到 maxStep 格
for (int step = 1; step <= maxStep; step++) {
int nr = r + dir[0] * step;
int nc = c + dir[1] * step;
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) break;
if (visited[nr][nc]) continue;
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc, steps + 1});
}
}
}
return -1; // 无法到达
}
/**
* 带障碍物的跳跃游戏(0表示障碍物,正数表示最大跳跃步数)
*/
public int minJumpsWithObstacles(int[] nums) {
int n = nums.length;
if (n <= 1) return 0;
if (nums[0] == 0) return -1;
int jumps = 0;
int currentEnd = 0;
int farthest = 0;
for (int i = 0; i < n - 1; i++) {
if (nums[i] == 0) continue; // 障碍物无法起跳
farthest = Math.max(farthest, i + nums[i]);
if (i == currentEnd) {
jumps++;
currentEnd = farthest;
if (currentEnd <= i) return -1;
}
}
return jumps;
}
/**
* 打印跳跃路径(回溯还原)
*/
public List<Integer> getJumpPath(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int[] from = new int[n]; // 记录从哪个位置跳来
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
from[0] = -1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (j + nums[j] >= i && dp[j] != Integer.MAX_VALUE) {
if (dp[j] + 1 < dp[i]) {
dp[i] = dp[j] + 1;
from[i] = j;
}
}
}
}
// 回溯路径
List<Integer> path = new ArrayList<>();
if (dp[n - 1] == Integer.MAX_VALUE) return path;
int cur = n - 1;
while (cur != -1) {
path.add(cur);
cur = from[cur];
}
Collections.reverse(path);
return path;
}
public static void main(String[] args) {
JumpRope solver = new JumpRope();
// 测试1:一维最少跳跃
int[] nums = {2, 3, 1, 1, 4};
System.out.println("=== 一维跳跃游戏 ===");
System.out.println("数组: " + Arrays.toString(nums));
System.out.println("是否可达: " + solver.canJump(nums));
System.out.println("最少跳跃(DP): " + solver.minJumpsDP(nums));
System.out.println("最少跳跃(贪心): " + solver.minJumpsGreedy(nums));
System.out.println("跳跃路径: " + solver.getJumpPath(nums));
// 测试2:二维网格
System.out.println("\n=== 二维网格跳皮筋 ===");
int[][] grid = {
{2, 1, 1},
{1, 1, 1},
{1, 1, 0}
};
System.out.println("最少步数: " + solver.minJumps2D(grid));
}
}
复杂度分析
| 问题类型 | 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 一维最少跳跃 | 动态规划 | O(n²) | O(n) |
| 一维最少跳跃 | 贪心 | O(n) | O(1) |
| 一维可达性 | 贪心 | O(n) | O(1) |
| 二维网格 | BFS | O(n × m × k) | O(n × m) |
贪心算法的核心洞察在于:不必关心具体从哪个位置起跳,只需维护当前跳数和下一步能到达的最远位置。这种”全局最优 = 局部最优”的性质是贪心策略生效的关键。
扩展思考
- 带权跳跃:不同跳跃消耗不同体力,使用 Dijkstra 算法求最小消耗路径。
- 双向跳跃:允许向前和向后跳跃,问题转化为无向图的最短路径。
- 多人博弈:多位玩家在网格上竞争到达终点,引入博弈论中的 minimax 策略。
- 随机跳跃:每次跳跃步数在 [1, nums[i]] 范围内随机,计算期望跳跃次数。