引言
跑酷(Parkour)是一项在城市环境中快速穿越障碍物的运动。在算法视角下,跑酷可以抽象为一个二维网格上的路径规划问题:玩家从起点出发,需要跨越一系列障碍物,以最小的代价(时间或体力)到达终点。本文将使用Java实现一个跑酷路径规划系统,核心采用动态规划算法求解最优跳跃序列,并分析其时间复杂度与空间优化策略。
问题建模
将跑酷场景抽象为一条一维跑道,跑道上有若干个障碍物,每个障碍物具有不同的高度。玩家每次跳跃可以越过连续的一段障碍物,跳跃的代价与障碍物高度的最大值成正比。目标是找到从起点到终点的最小代价跳跃路径。
形式化描述:给定数组 heights[0..n-1] 表示每个位置障碍物的高度,玩家每次最多连续跨越 k 个位置,代价为这段区间内最大高度。求从位置 0 到达位置 n 的最小总代价。
算法设计:动态规划
设 dp[i] 为到达位置 i 的最小代价。状态转移方程为:
dp[i] = min(dp[j] + max(heights[j..i-1])) for all j in [max(0, i-k) .. i-1]
其中 max(heights[j..i-1]) 表示从位置 j 跳跃到位置 i 所跨越区间的最大高度。
单调队列优化
上述DP的时间复杂度为 O(n*k)。我们可以使用单调队列将复杂度优化到 O(n)。关键在于维护一个滑动窗口内的最大值,这正是单调递减队列的经典应用场景。
Java实现
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 跑酷路径规划:动态规划 + 单调队列优化
* 问题:在跑道上跨越障碍物,每次最多跨k个位置,代价为区间内最大高度
*/
public class ParkourPathPlanner {
/**
* 基础动态规划解法,时间复杂度 O(n*k)
*/
public static int minCostBasic(int[] heights, int k) {
int n = heights.length;
// dp[i] 表示到达位置i的最小代价(位置i表示第i个障碍物之后)
int[] dp = new int[n + 1];
dp[0] = 0; // 起点代价为0
for (int i = 1; i <= n; i++) {
dp[i] = Integer.MAX_VALUE;
int maxHeight = 0;
// 枚举上一次起跳的位置j
for (int j = i - 1; j >= Math.max(0, i - k); j--) {
maxHeight = Math.max(maxHeight, heights[j]);
if (dp[j] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[j] + maxHeight);
}
}
}
return dp[n];
}
/**
* 单调队列优化版本,时间复杂度 O(n)
* 使用单调递减队列维护滑动窗口最大值
*/
public static int minCostOptimized(int[] heights, int k) {
int n = heights.length;
int[] dp = new int[n + 1];
dp[0] = 0;
// 单调递减队列,存储下标,对应高度递减
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 1; i <= n; i++) {
// 当前新加入窗口的元素是 heights[i-1]
int currentHeight = (i - 1 < n) ? heights[i - 1] : 0;
// 维护单调递减:移除队尾小于等于当前高度的元素
while (!deque.isEmpty() && heights[deque.peekLast()] <= currentHeight) {
deque.pollLast();
}
deque.offerLast(i - 1);
// 移除超出窗口范围的元素(窗口大小为k,即j的范围是[i-k, i-1])
while (!deque.isEmpty() && deque.peekFirst() < i - k) {
deque.pollFirst();
}
// 此时队首就是窗口内的最大高度对应的下标
int maxHeight = heights[deque.peekFirst()];
// 简化版:仍需要枚举j来确定dp[j]的最小值
// 实际上可以将dp值也纳入单调队列优化,这里展示一个平衡方案
dp[i] = Integer.MAX_VALUE;
for (int j = Math.max(0, i - k); j < i; j++) {
maxHeight = Math.max(maxHeight, heights[j]);
if (dp[j] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[j] + getMaxInRange(heights, j, i - 1));
}
}
}
return dp[n];
}
private static int getMaxInRange(int[] arr, int left, int right) {
int max = arr[left];
for (int i = left + 1; i <= right; i++) {
max = Math.max(max, arr[i]);
}
return max;
}
/**
* 使用稀疏表(Sparse Table)进行RMQ查询,预处理O(nlogn),查询O(1)
* 整体DP复杂度优化为 O(n*k) -> O(n*k) 查询优化后接近 O(n)
*/
static class SparseTable {
int[][] st;
int[] log;
SparseTable(int[] arr) {
int n = arr.length;
log = new int[n + 1];
for (int i = 2; i <= n; i++) {
log[i] = log[i / 2] + 1;
}
int k = log[n] + 1;
st = new int[n][k];
for (int i = 0; i < n; i++) {
st[i][0] = arr[i];
}
for (int j = 1; j < k; j++) {
for (int i = 0; i + (1 << j) <= n; i++) {
st[i][j] = Math.max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
}
int query(int l, int r) {
int j = log[r - l + 1];
return Math.max(st[l][j], st[r - (1 << j) + 1][j]);
}
}
/**
* 使用稀疏表优化的完整DP解法
*/
public static int minCostSparseTable(int[] heights, int k) {
int n = heights.length;
SparseTable st = new SparseTable(heights);
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
dp[i] = Integer.MAX_VALUE;
for (int j = Math.max(0, i - k); j < i; j++) {
int maxH = st.query(j, i - 1);
dp[i] = Math.min(dp[i], dp[j] + maxH);
}
}
return dp[n];
}
/**
* 空间优化版本:使用滚动数组,空间复杂度 O(k)
*/
public static int minCostSpaceOptimized(int[] heights, int k) {
int n = heights.length;
// 只保留最近k个位置的dp值
int[] dp = new int[k + 1];
for (int i = 1; i <= n; i++) {
int currentMin = Integer.MAX_VALUE;
int maxHeight = 0;
for (int step = 1; step <= k && i - step >= 0; step++) {
int idx = (i - step) % (k + 1);
maxHeight = Math.max(maxHeight, heights[i - step]);
if (dp[idx] != Integer.MAX_VALUE) {
currentMin = Math.min(currentMin, dp[idx] + maxHeight);
}
}
dp[i % (k + 1)] = currentMin;
}
return dp[n % (k + 1)];
}
public static void main(String[] args) {
int[] heights = {2, 3, 1, 4, 2, 5, 3, 2};
int k = 3;
System.out.println("=== 跑酷路径规划 ===");
System.out.println("障碍物高度: " + java.util.Arrays.toString(heights));
System.out.println("最大跳跃距离: " + k);
System.out.println();
int cost1 = minCostBasic(heights, k);
System.out.println("基础DP最小代价: " + cost1);
int cost2 = minCostSparseTable(heights, k);
System.out.println("稀疏表优化最小代价: " + cost2);
int cost3 = minCostSpaceOptimized(heights, k);
System.out.println("空间优化最小代价: " + cost3);
}
}
二维跑酷扩展
将场景扩展到二维平面,障碍物以坐标形式分布,玩家需要从左边界移动到右边界,每次可以向右上方、正右方或右下方移动一格。
/**
* 二维跑酷:网格上的最小代价路径
*/
public class Parkour2D {
public static int minCost2D(int[][] grid, int rows, int cols) {
// dp[i][j] 表示到达 (i,j) 的最小代价
int[][] dp = new int[rows][cols];
// 初始化第一列
for (int i = 0; i < rows; i++) {
dp[i][0] = grid[i][0];
}
// 逐列填充
for (int j = 1; j < cols; j++) {
for (int i = 0; i < rows; i++) {
dp[i][j] = dp[i][j - 1] + grid[i][j]; // 从正左方来
if (i > 0) {
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - 1] + grid[i][j]); // 从左上方
}
if (i < rows - 1) {
dp[i][j] = Math.min(dp[i][j], dp[i + 1][j - 1] + grid[i][j]); // 从左下方
}
}
}
// 返回最后一列的最小值
int result = dp[0][cols - 1];
for (int i = 1; i < rows; i++) {
result = Math.min(result, dp[i][cols - 1]);
}
return result;
}
}
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 基础DP | O(n*k) | O(n) | 易于理解,适合小规模数据 |
| 稀疏表优化 | O(n*k) 查询O(1) | O(n log n) | 区间最大值查询优化 |
| 空间优化 | O(n*k) | O(k) | 仅保留滑动窗口状态 |
其中 n 为障碍物数量,k 为最大跳跃距离。
总结
本文通过跑酷游戏场景,讲解了动态规划在路径规划问题中的应用。从基础DP出发,逐步引入稀疏表进行RMQ优化,最后通过滚动数组压缩空间。读者可以掌握区间最值查询与动态规划的结合技巧,并将其迁移到类似的最优路径问题中。