每日算法 — 使用java实现攀岩:图论节点覆盖与动态规划最优能量路径

问题描述

攀岩路线由岩壁上的多个支点(手点和脚点)组成,攀岩者需要在这些支点之间移动以到达顶点。每个支点有不同的属性:位置坐标、握持难度、体力消耗值。从当前支点到下一个支点的移动也有对应的体力消耗,取决于距离和角度。

本文将用Java实现攀岩路线的最优能量消耗规划,将岩壁建模为带权有向图,使用Dijkstra算法寻找从起点到终点的最小能量路径,并通过动态规划处理多阶段休息恢复策略。

核心算法思路

图论建模

将每个支点建模为图中的一个节点。若两个支点之间的欧氏距离不超过攀岩者的最大臂展,则在它们之间建立一条有向边,边的权重为从源点到目标点的体力消耗:

$$cost(u, v) = baseCost \times distance(u, v) \times anglePenalty(u, v)$$

其中 $anglePenalty$ 根据移动方向的陡峭程度增加额外消耗(向上攀爬比横向移动更费力)。

Dijkstra最短路径

由于所有边的权重均为正,Dijkstra算法可以在 $O((V+E) \log V)$ 时间内找到从起点到终点的最小能量路径。

动态规划休息策略

攀岩者在途中可以选择在某些支点上休息恢复体力。设 $dp[i][e]$ 表示到达第 $i$ 个支点且剩余体力为 $e$ 时的最大累计恢复值。状态转移考虑两种选择:
– 继续前进:消耗边权重对应的体力
– 休息恢复:在当前支点恢复一定比例的体力(但耗时增加)

Java实现

import java.util.*;

/**
 * 攀岩最优能量路径规划器
 * 使用图论建模、Dijkstra最短路径与动态规划休息策略
 */
public class ClimbingRoutePlanner {

    // 支点类
    public static class Hold {
        public final int id;
        public final double x, y;      // 坐标位置 (米)
        public final double difficulty; // 握持难度系数 (1.0~3.0)
        public final double restValue;  // 休息恢复效率 (0~1)

        public Hold(int id, double x, double y, double difficulty, double restValue) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.difficulty = difficulty;
            this.restValue = restValue;
        }

        public double distanceTo(Hold other) {
            double dx = this.x - other.x;
            double dy = this.y - other.y;
            return Math.sqrt(dx * dx + dy * dy);
        }
    }

    // 边类
    public static class Edge {
        public final int from, to;
        public final double cost;

        public Edge(int from, int to, double cost) {
            this.from = from;
            this.to = to;
            this.cost = cost;
        }
    }

    private List<Hold> holds;
    private List<List<Edge>> graph;
    private double maxReach;      // 最大臂展 (米)
    private double baseCost;      // 基础体力消耗系数

    public ClimbingRoutePlanner(double maxReach, double baseCost) {
        this.maxReach = maxReach;
        this.baseCost = baseCost;
    }

    /**
     * 添加支点列表并构建图
     */
    public void setHolds(List<Hold> holds) {
        this.holds = holds;
        this.graph = new ArrayList<>();
        for (int i = 0; i < holds.size(); i++) {
            graph.add(new ArrayList<>());
        }
        buildGraph();
    }

    /**
     * 构建带权有向图
     */
    private void buildGraph() {
        for (int i = 0; i < holds.size(); i++) {
            for (int j = 0; j < holds.size(); j++) {
                if (i == j) continue;

                double dist = holds.get(i).distanceTo(holds.get(j));
                if (dist <= maxReach) {
                    double cost = computeCost(i, j, dist);
                    graph.get(i).add(new Edge(i, j, cost));
                }
            }
        }
    }

    /**
     * 计算从支点u到v的体力消耗
     */
    private double computeCost(int u, int v, double dist) {
        Hold from = holds.get(u);
        Hold to = holds.get(v);

        // 基础距离消耗
        double cost = baseCost * dist;

        // 握持难度惩罚
        cost *= to.difficulty;

        // 角度惩罚:向上攀爬更费力
        double dy = to.y - from.y;
        double dx = to.x - from.x;
        double angle = Math.atan2(dy, dx);

        // 向上角度越大,惩罚越高
        if (angle > 0) {
            cost *= (1 + Math.sin(angle));
        }

        // 横向移动的微调惩罚
        if (Math.abs(angle) < Math.toRadians(15)) {
            cost *= 1.2;
        }

        return cost;
    }

    /**
     * Dijkstra算法寻找最小能量路径
     * @param start 起点索引
     * @param target 终点索引
     * @return 最短路径结果
     */
    public ShortestPathResult dijkstra(int start, int target) {
        int n = holds.size();
        double[] dist = new double[n];
        int[] parent = new int[n];
        boolean[] visited = new boolean[n];

        Arrays.fill(dist, Double.MAX_VALUE);
        Arrays.fill(parent, -1);
        dist[start] = 0;

        // 优先队列: (当前距离, 节点)
        PriorityQueue<double[]> pq = new PriorityQueue<>(Comparator.comparingDouble(a -> a[0]));
        pq.offer(new double[]{0, start});

        while (!pq.isEmpty()) {
            double[] curr = pq.poll();
            double d = curr[0];
            int u = (int) curr[1];

            if (visited[u]) continue;
            visited[u] = true;

            if (u == target) break;

            for (Edge e : graph.get(u)) {
                int v = e.to;
                if (visited[v]) continue;

                double newDist = dist[u] + e.cost;
                if (newDist < dist[v]) {
                    dist[v] = newDist;
                    parent[v] = u;
                    pq.offer(new double[]{newDist, v});
                }
            }
        }

        // 重构路径
        List<Integer> path = new ArrayList<>();
        int curr = target;
        while (curr != -1) {
            path.add(curr);
            curr = parent[curr];
        }
        Collections.reverse(path);

        return new ShortestPathResult(dist[target], path);
    }

    /**
     * 带休息策略的动态规划
     * 在体力限制下寻找最优路径
     * @param start 起点
     * @param target 终点
     * @param maxEnergy 最大体力值
     * @return 可行路径及剩余体力
     */
    public RestStrategyResult findOptimalPathWithRest(int start, int target, double maxEnergy) {
        int n = holds.size();
        // dp[i] = 到达i时的最小累计消耗
        double[] dp = new double[n];
        int[] parent = new int[n];
        boolean[] canReach = new boolean[n];

        Arrays.fill(dp, Double.MAX_VALUE);
        Arrays.fill(parent, -1);
        dp[start] = 0;
        canReach[start] = true;

        // 使用类似Dijkstra的扩展,但加入体力约束
        PriorityQueue<double[]> pq = new PriorityQueue<>(Comparator.comparingDouble(a -> a[0]));
        pq.offer(new double[]{0, start, maxEnergy}); // (累计消耗, 节点, 剩余体力)

        while (!pq.isEmpty()) {
            double[] curr = pq.poll();
            double costSoFar = curr[0];
            int u = (int) curr[1];
            double energyLeft = curr[2];

            for (Edge e : graph.get(u)) {
                int v = e.to;
                double moveCost = e.cost;

                // 检查是否有足够体力
                if (energyLeft >= moveCost) {
                    double newCost = costSoFar + moveCost;
                    double newEnergy = energyLeft - moveCost;

                    // 考虑在v休息
                    Hold holdV = holds.get(v);
                    if (holdV.restValue > 0) {
                        double recovered = maxEnergy * holdV.restValue;
                        newEnergy = Math.min(maxEnergy, newEnergy + recovered);
                    }

                    if (newCost < dp[v]) {
                        dp[v] = newCost;
                        parent[v] = u;
                        canReach[v] = true;
                        pq.offer(new double[]{newCost, v, newEnergy});
                    }
                }
            }
        }

        if (!canReach[target]) {
            return new RestStrategyResult(-1, new ArrayList<>(), 0);
        }

        // 重构路径
        List<Integer> path = new ArrayList<>();
        int curr = target;
        while (curr != -1) {
            path.add(curr);
            curr = parent[curr];
        }
        Collections.reverse(path);

        return new RestStrategyResult(dp[target], path, maxEnergy - dp[target]);
    }

    /**
     * 打印路线信息
     */
    public void printRoute(ShortestPathResult result) {
        System.out.println("=== 攀岩路线规划 ===");
        System.out.printf("最小能量消耗: %.2f J%n", result.totalCost);
        System.out.println("路径支点: ");

        for (int i = 0; i < result.path.size(); i++) {
            int holdId = result.path.get(i);
            Hold h = holds.get(holdId);
            System.out.printf("  %d. 支点#%d (%.1f, %.1f) 难度:%.1f 休息效率:%.1f%% %s%n",
                i + 1, h.id, h.x, h.y, h.difficulty, h.restValue * 100,
                h.restValue > 0.3 ? "[推荐休息]" : "");
        }

        // 计算路径统计
        double totalDist = 0;
        double maxStep = 0;
        for (int i = 1; i < result.path.size(); i++) {
            Hold from = holds.get(result.path.get(i-1));
            Hold to = holds.get(result.path.get(i));
            double d = from.distanceTo(to);
            totalDist += d;
            maxStep = Math.max(maxStep, d);
        }

        System.out.printf("\n总攀爬距离: %.2f m%n", totalDist);
        System.out.printf("最大单步距离: %.2f m%n", maxStep);
        System.out.printf("平均单步消耗: %.2f J%n",
            result.totalCost / (result.path.size() - 1));
    }

    /**
     * 生成随机攀岩路线
     */
    public static List<Hold> generateRandomRoute(int numHolds, double wallWidth,
                                                  double wallHeight, long seed) {
        Random rand = new Random(seed);
        List<Hold> holds = new ArrayList<>();

        // 起点 (底部中央)
        holds.add(new Hold(0, wallWidth / 2, 0, 1.0, 0));

        // 中间支点
        for (int i = 1; i < numHolds - 1; i++) {
            double x = rand.nextDouble() * wallWidth;
            double y = (rand.nextDouble() * 0.8 + 0.1) * wallHeight;
            double diff = 1.0 + rand.nextDouble() * 2.0;
            double rest = rand.nextDouble() > 0.6 ? rand.nextDouble() * 0.5 : 0;
            holds.add(new Hold(i, x, y, diff, rest));
        }

        // 终点 (顶部)
        holds.add(new Hold(numHolds - 1, wallWidth / 2, wallHeight, 1.0, 0));

        return holds;
    }

    // 结果类
    public static class ShortestPathResult {
        public final double totalCost;
        public final List<Integer> path;

        public ShortestPathResult(double totalCost, List<Integer> path) {
            this.totalCost = totalCost;
            this.path = path;
        }
    }

    public static class RestStrategyResult {
        public final double totalCost;
        public final List<Integer> path;
        public final double remainingEnergy;

        public RestStrategyResult(double totalCost, List<Integer> path, double remainingEnergy) {
            this.totalCost = totalCost;
            this.path = path;
            this.remainingEnergy = remainingEnergy;
        }
    }

    public static void main(String[] args) {
        // 创建攀岩规划器:最大臂展1.5米,基础消耗系数10
        ClimbingRoutePlanner planner = new ClimbingRoutePlanner(1.5, 10.0);

        // 生成20个支点的随机路线,岩壁宽5米高15米
        List<Hold> route = generateRandomRoute(20, 5.0, 15.0, 42);
        planner.setHolds(route);

        // Dijkstra最优路径
        System.out.println("=== Dijkstra最小能量路径 ===");
        ShortestPathResult sp = planner.dijkstra(0, route.size() - 1);
        planner.printRoute(sp);

        // 带休息策略的路径
        System.out.println("\n=== 带休息策略的路径 (体力限制: 500) ===");
        RestStrategyResult restResult = planner.findOptimalPathWithRest(
            0, route.size() - 1, 500);

        if (restResult.totalCost >= 0) {
            System.out.printf("可行路径找到!总消耗: %.2f J, 剩余体力: %.2f%n",
                restResult.totalCost, restResult.remainingEnergy);
            System.out.println("路径: " + restResult.path);
        } else {
            System.out.println("在体力限制下无法到达终点,需要增加体力或改进路线!");
        }
    }
}

算法复杂度分析

  • 建图:$O(V^2)$,需要计算所有支点对之间的距离并判断是否在臂展范围内。
  • Dijkstra算法:$O((V + E) \log V)$,使用优先队列实现。对于岩壁支点图,$E \leq V^2$。
  • 带休息策略的DP:最坏情况下 $O(E \cdot K)$,$K$ 为体力离散化的精度。若使用连续体力值,复杂度退化为与Dijkstra相同的 $O((V+E) \log V)$。

扩展思考

  1. 三维岩壁:将支点坐标扩展为 $(x, y, z)$ 三维空间,距离计算改为三维欧氏距离,算法框架保持不变。
  2. 动态支点:若某些支点在受力后会晃动(增加握持难度),可将难度系数建模为随机变量,使用期望最短路径求解。
  3. 多攀岩者协作:在团队攀岩中,可以使用多智能体路径规划(MAPF)避免绳索缠绕和路线冲突。
  4. 疲劳累积模型:引入非线性疲劳函数 $f(t) = e^{\lambda t}$,使长时间连续攀爬的边际成本递增,更接近真实生理极限。

总结

攀岩路线规划将图论最短路径与动态规划资源管理巧妙融合。Dijkstra算法在正权图中保证了全局最优性,而休息恢复策略通过状态扩展将体力约束纳入路径评估。这种带资源约束的最短路径框架广泛应用于物流配送(燃油限制)、无人机巡航(电量限制)和角色扮演游戏(魔法值管理)等场景。