每日算法 — 使用java实现滑雪:动态规划最优路径规划与贪心障碍物规避

问题描述

滑雪赛道由一条带有障碍物和加速带的网格表示。滑雪者从山顶出发,每次可以向左下、正下或右下三个方向滑行一格。每个格子有不同的分值:障碍物为负分、普通雪道为0分、加速带为正分。目标是找到从起点到终点的最大得分路径。

本文将用Java实现滑雪最优路径求解,使用动态规划计算最大得分路径,并结合贪心策略进行实时障碍物规避。

核心算法思路

动态规划模型

设 $dp[i][j]$ 表示到达第 $i$ 行第 $j$ 列时的最大得分。状态转移方程为:

$$dp[i][j] = grid[i][j] + \max(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1])$$

边界条件:$dp[0][j] = grid[0][j]$(第一行直接初始化)。

路径重构

通过记录每个状态的前驱方向,从终点回溯即可得到最优路径。

贪心障碍物规避

在动态规划的基础上,若检测到前方存在高密度障碍物区域,贪心策略会优先选择障碍物较少的通道,即使该通道的总得分略低。这种折中策略在真实滑雪场景中更具鲁棒性。

Java实现

import java.util.*;

/**
 * 滑雪最优路径求解器
 * 使用动态规划计算最大得分路径,支持贪心障碍物规避策略
 */
public class SkiPathPlanner {

    // 格子类型
    public static final int OBSTACLE = -10;   // 障碍物(树木/岩石)
    public static final int NORMAL = 0;       // 普通雪道
    public static final int BOOST = 5;        // 加速带
    public static final int SUPER_BOOST = 10; // 超级加速带
    public static final int CLIFF = -50;      // 悬崖(必须规避)

    private int[][] grid;
    private int rows;
    private int cols;

    public SkiPathPlanner(int[][] grid) {
        this.grid = grid;
        this.rows = grid.length;
        this.cols = grid[0].length;
    }

    /**
     * 动态规划求解最大得分路径
     * @return 包含最大得分和路径的结果对象
     */
    public PathResult findOptimalPathDP() {
        int[][] dp = new int[rows][cols];
        int[][] parent = new int[rows][cols]; // 记录前驱列号

        // 初始化第一行
        for (int j = 0; j < cols; j++) {
            dp[0][j] = grid[0][j];
            parent[0][j] = -1;
        }

        // 动态规划填充
        for (int i = 1; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                int maxPrev = Integer.MIN_VALUE;
                int bestK = j;

                // 检查三个可能的前驱方向:左上、正上、右上
                for (int delta = -1; delta <= 1; delta++) {
                    int prevJ = j + delta;
                    if (prevJ >= 0 && prevJ < cols) {
                        if (dp[i-1][prevJ] > maxPrev) {
                            maxPrev = dp[i-1][prevJ];
                            bestK = prevJ;
                        }
                    }
                }

                dp[i][j] = grid[i][j] + maxPrev;
                parent[i][j] = bestK;
            }
        }

        // 寻找终点的最大值
        int maxScore = Integer.MIN_VALUE;
        int endCol = 0;
        for (int j = 0; j < cols; j++) {
            if (dp[rows-1][j] > maxScore) {
                maxScore = dp[rows-1][j];
                endCol = j;
            }
        }

        // 重构路径
        List<int[]> path = new ArrayList<>();
        int currCol = endCol;
        for (int i = rows - 1; i >= 0; i--) {
            path.add(new int[]{i, currCol});
            currCol = parent[i][currCol];
        }
        Collections.reverse(path);

        return new PathResult(maxScore, path, dp);
    }

    /**
     * 贪心障碍物规避策略
     * 在局部范围内选择障碍物密度最低的方向
     */
    public PathResult findGreedyPathWithObstacleAvoidance(int lookahead) {
        List<int[]> path = new ArrayList<>();
        int currRow = 0;
        int currCol = cols / 2; // 从中间出发
        int totalScore = grid[currRow][currCol];
        path.add(new int[]{currRow, currCol});

        while (currRow < rows - 1) {
            int bestDelta = 0;
            double bestScore = Double.NEGATIVE_INFINITY;

            for (int delta = -1; delta <= 1; delta++) {
                int nextCol = currCol + delta;
                if (nextCol < 0 || nextCol >= cols) continue;

                // 计算lookahead范围内的综合得分
                double score = evaluatePathSegment(currRow + 1, nextCol, lookahead);

                // 严重惩罚悬崖
                if (grid[currRow + 1][nextCol] == CLIFF) {
                    score -= 1000;
                }

                if (score > bestScore) {
                    bestScore = score;
                    bestDelta = delta;
                }
            }

            currCol += bestDelta;
            currRow++;
            totalScore += grid[currRow][currCol];
            path.add(new int[]{currRow, currCol});
        }

        return new PathResult(totalScore, path, null);
    }

    /**
     * 评估从指定位置开始的lookahead段路径质量
     */
    private double evaluatePathSegment(int startRow, int startCol, int lookahead) {
        double score = 0;
        int count = 0;

        for (int i = 0; i < lookahead && startRow + i < rows; i++) {
            int r = startRow + i;
            // 在每一行考虑当前列及左右邻居
            for (int delta = -1; delta <= 1; delta++) {
                int c = startCol + delta;
                if (c >= 0 && c < cols) {
                    score += grid[r][c];
                    count++;
                }
            }
        }

        return count > 0 ? score / count : 0;
    }

    /**
     * 混合策略:DP全局最优 + 贪心局部调整
     */
    public PathResult findHybridPath(double obstacleThreshold) {
        // 先计算DP最优路径
        PathResult dpResult = findOptimalPathDP();
        List<int[]> dpPath = dpResult.path;

        // 检查路径上是否有过多障碍物
        int obstacleCount = 0;
        for (int[] pos : dpPath) {
            if (grid[pos[0]][pos[1]] < 0) obstacleCount++;
        }

        double obstacleRatio = (double) obstacleCount / dpPath.size();

        // 如果障碍物比例超过阈值,使用贪心策略
        if (obstacleRatio > obstacleThreshold) {
            System.out.println("DP路径障碍物过多 (" + String.format("%.1f%%", obstacleRatio * 100)
                + "),切换至贪心规避策略");
            return findGreedyPathWithObstacleAvoidance(3);
        }

        return dpResult;
    }

    /**
     * 打印赛道和路径
     */
    public void printTrack(PathResult result) {
        boolean[][] onPath = new boolean[rows][cols];
        if (result.path != null) {
            for (int[] pos : result.path) {
                onPath[pos[0]][pos[1]] = true;
            }
        }

        System.out.println("=== 滑雪赛道 ===");
        System.out.println("图例: ★起点/终点 ○普通 ▲加速 ■障碍 ▼悬崖 ☆路径");
        System.out.println();

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (onPath[i][j]) {
                    if (i == 0) System.out.print(" S ");
                    else if (i == rows - 1) System.out.print(" E ");
                    else System.out.print(" * ");
                } else {
                    switch (grid[i][j]) {
                        case OBSTACLE: System.out.print(" T "); break;
                        case NORMAL: System.out.print(" . "); break;
                        case BOOST: System.out.print(" + "); break;
                        case SUPER_BOOST: System.out.print(" ++"); break;
                        case CLIFF: System.out.print(" C "); break;
                        default: System.out.printf("%2d ", grid[i][j]);
                    }
                }
            }
            System.out.println();
        }

        System.out.println("\n总得分: " + result.score);
        System.out.println("路径长度: " + result.path.size() + " 步");
    }

    /**
     * 生成随机赛道
     */
    public static int[][] generateRandomTrack(int rows, int cols, long seed) {
        Random rand = new Random(seed);
        int[][] grid = new int[rows][cols];

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                double r = rand.nextDouble();
                if (r < 0.15) grid[i][j] = OBSTACLE;
                else if (r < 0.20) grid[i][j] = CLIFF;
                else if (r < 0.35) grid[i][j] = BOOST;
                else if (r < 0.40) grid[i][j] = SUPER_BOOST;
                else grid[i][j] = NORMAL;
            }
        }

        // 确保起点和终点安全
        grid[0][cols/2] = NORMAL;
        grid[rows-1][cols/2] = NORMAL;

        return grid;
    }

    // 结果类
    public static class PathResult {
        public final int score;
        public final List<int[]> path;
        public final int[][] dpTable;

        public PathResult(int score, List<int[]> path, int[][] dpTable) {
            this.score = score;
            this.path = path;
            this.dpTable = dpTable;
        }
    }

    public static void main(String[] args) {
        // 生成15x10的随机赛道
        int[][] track = generateRandomTrack(15, 10, 12345);
        SkiPathPlanner planner = new SkiPathPlanner(track);

        System.out.println("=== 动态规划最优路径 ===");
        PathResult dpResult = planner.findOptimalPathDP();
        planner.printTrack(dpResult);

        System.out.println("\n=== 贪心障碍物规避路径 ===");
        PathResult greedyResult = planner.findGreedyPathWithObstacleAvoidance(3);
        planner.printTrack(greedyResult);

        System.out.println("\n=== 混合策略路径 ===");
        PathResult hybridResult = planner.findHybridPath(0.3);
        planner.printTrack(hybridResult);
    }
}

算法复杂度分析

  • 动态规划:时间复杂度 $O(m \times n)$,空间复杂度 $O(m \times n)$,其中 $m$ 为行数,$n$ 为列数。每个格子只需计算三个前驱状态的最大值。
  • 贪心策略:时间复杂度 $O(m \times n \times k)$,$k$ 为lookahead深度。每步需要评估前方 $k$ 行的局部区域。
  • 混合策略:先执行DP($O(mn)$),若障碍物比例过高再执行贪心($O(mnk)$),最坏情况下为 $O(mnk)$。

扩展思考

  1. 多目标优化:除得分外,还可引入路径平滑度指标(减少急转弯)、最短时间指标等,使用加权目标函数或帕累托最优求解。
  2. 随机赛道生成:可通过柏林噪声(Perlin Noise)生成更自然的山地地形,而非完全随机分布。
  3. 实时重规划:若滑雪过程中发现前方出现意外障碍(如雪崩),可使用A*算法从当前位置重新规划到终点。
  4. 概率模型:将障碍物视为概率分布而非确定值,使用期望动态规划(EDP)计算最大期望得分路径。

总结

滑雪路径规划将动态规划与贪心策略有机结合:DP保证全局最优性,贪心策略在障碍物密集区域提供鲁棒的局部规避能力。混合策略通过阈值判断自动切换两种模式,兼顾了最优性与安全性。这种分层决策框架广泛应用于机器人路径规划、游戏AI和导航系统中。