每日算法 — 使用java实现登山:图论建模与动态规划最优能量路径

一、问题背景

登山者从山脚出发前往山顶,需要选择一条最优路径。路径的选择不仅要考虑距离最短,还要考虑海拔变化带来的能量消耗。上坡消耗能量,下坡可以部分恢复能量(但膝盖损耗限制恢复效率)。本文用Java实现登山路径的图论建模,并用动态规划求解能量最优路径。

二、核心算法思路

2.1 图论建模

将登山地形建模为带权无向图:
– 节点:地形采样点(具有海拔属性)
– 边:相邻采样点之间的可通行路径
– 边权:通过该路段所需的净能量消耗

2.2 能量计算模型

能量消耗由以下因素决定:
– 上坡:E_up = m * g * Δh / η,其中 η 为人体能量转换效率(约25%)
– 下坡:E_down = m * g * Δh * (1 - recoveryRate),回收部分势能
– 平路:基础代谢能量 E_flat = c * distance

2.3 动态规划求解

使用Dijkstra算法的变种求解能量最优路径。由于能量消耗满足最优子结构,状态定义为:dp[node][energyLevel] = 到达该节点的最小总能量消耗。

为简化实现,本文采用标准Dijkstra算法,以能量消耗作为边权。

三、Java实现

import java.util.*;

/**
 * 登山路径规划:图论建模与动态规划最优能量路径
 */
public class MountainClimbingDP {

    // 物理常量
    private static final double GRAVITY = 9.8;
    private static final double HUMAN_EFFICIENCY = 0.25;
    private static final double RECOVERY_RATE = 0.15;
    private static final double BASE_METABOLIC_COST = 3.5; // J/(kg·m)

    /**
     * 地形节点
     */
    static class Node {
        int id;
        double x, y;      // 平面坐标 (m)
        double elevation; // 海拔 (m)

        Node(int id, double x, double y, double elevation) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.elevation = elevation;
        }

        /**
         * 计算到另一节点的平面距离
         */
        double distanceTo(Node other) {
            double dx = this.x - other.x;
            double dy = this.y - other.y;
            return Math.sqrt(dx * dx + dy * dy);
        }

        /**
         * 计算到另一节点的坡度角
         */
        double slopeAngleTo(Node other) {
            double dist = distanceTo(other);
            if (dist == 0) return 0;
            double dh = other.elevation - this.elevation;
            return Math.atan2(dh, dist);
        }
    }

    /**
     * 边:连接两个地形节点
     */
    static class Edge {
        Node from, to;
        double energyCost; // 通过该边的净能量消耗 (J/kg)

        Edge(Node from, Node to) {
            this.from = from;
            this.to = to;
            this.energyCost = computeEnergyCost();
        }

        /**
         * 计算单位质量通过该边的能量消耗
         */
        private double computeEnergyCost() {
            double dist = from.distanceTo(to);
            double dh = to.elevation - from.elevation;
            double baseCost = BASE_METABOLIC_COST * dist;

            if (dh > 0) {
                // 上坡:需要额外克服重力做功,考虑人体效率
                double climbCost = GRAVITY * dh / HUMAN_EFFICIENCY;
                return baseCost + climbCost;
            } else {
                // 下坡:势能部分回收,但仍有基础代谢和膝盖损耗
                double descentRecovery = GRAVITY * Math.abs(dh) * RECOVERY_RATE;
                return baseCost - descentRecovery;
            }
        }
    }

    /**
     * 地形图
     */
    static class TerrainGraph {
        List<Node> nodes = new ArrayList<>();
        Map<Integer, List<Edge>> adjacency = new HashMap<>();

        void addNode(Node node) {
            nodes.add(node);
            adjacency.put(node.id, new ArrayList<>());
        }

        void addEdge(Node a, Node b) {
            Edge e1 = new Edge(a, b);
            Edge e2 = new Edge(b, a);
            adjacency.get(a.id).add(e1);
            adjacency.get(b.id).add(e2);
        }

        /**
         * 获取邻居边
         */
        List<Edge> getNeighbors(int nodeId) {
            return adjacency.getOrDefault(nodeId, new ArrayList<>());
        }
    }

    /**
     * Dijkstra算法求解能量最优路径
     */
    static class PathFinder {

        /**
         * 返回从起点到终点的最优路径及总能量消耗
         */
        Result findOptimalPath(TerrainGraph graph, int startId, int goalId) {
            int n = graph.nodes.size();
            double[] dist = new double[n];
            int[] prev = new int[n];
            boolean[] visited = new boolean[n];

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

            // 优先队列:按能量消耗排序
            PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingDouble(a -> dist[a[0]]));
            pq.offer(new int[]{startId});

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

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

                if (u == goalId) break;

                for (Edge edge : graph.getNeighbors(u)) {
                    int v = edge.to.id;
                    if (visited[v]) continue;

                    double newDist = dist[u] + edge.energyCost;
                    if (newDist < dist[v]) {
                        dist[v] = newDist;
                        prev[v] = u;
                        pq.offer(new int[]{v});
                    }
                }
            }

            // 重建路径
            List<Integer> path = new ArrayList<>();
            for (int at = goalId; at != -1; at = prev[at]) {
                path.add(at);
            }
            Collections.reverse(path);

            return new Result(path, dist[goalId]);
        }
    }

    static class Result {
        List<Integer> path;
        double totalEnergy;

        Result(List<Integer> path, double totalEnergy) {
            this.path = path;
            this.totalEnergy = totalEnergy;
        }
    }

    /**
     * 生成示例地形:螺旋山形
     */
    static TerrainGraph generateSpiralMountain(int layers) {
        TerrainGraph graph = new TerrainGraph();
        Random rand = new Random(42);

        // 创建节点
        int id = 0;
        for (int layer = 0; layer <= layers; layer++) {
            double radius = (layers - layer) * 50.0;
            double elevation = layer * 100.0 + rand.nextDouble() * 20;
            int pointsInLayer = Math.max(6, layer * 4);

            for (int i = 0; i < pointsInLayer; i++) {
                double angle = 2 * Math.PI * i / pointsInLayer;
                double x = radius * Math.cos(angle);
                double y = radius * Math.sin(angle);
                graph.addNode(new Node(id++, x, y, elevation));
            }
        }

        // 连接相邻层的节点
        int idx = 0;
        for (int layer = 0; layer < layers; layer++) {
            int currPoints = Math.max(6, layer * 4);
            int nextPoints = Math.max(6, (layer + 1) * 4);
            int currStart = idx;
            int nextStart = idx + currPoints;

            for (int i = 0; i < currPoints; i++) {
                Node currNode = graph.nodes.get(currStart + i);
                // 连接到下一层的最近节点
                for (int j = 0; j < nextPoints; j++) {
                    Node nextNode = graph.nodes.get(nextStart + j);
                    double dist = currNode.distanceTo(nextNode);
                    if (dist < 80) {
                        graph.addEdge(currNode, nextNode);
                    }
                }
                // 连接同层的相邻节点
                Node nextInLayer = graph.nodes.get(currStart + (i + 1) % currPoints);
                graph.addEdge(currNode, nextInLayer);
            }
            idx += currPoints;
        }

        return graph;
    }

    public static void main(String[] args) {
        // 生成地形
        TerrainGraph mountain = generateSpiralMountain(8);
        System.out.println("=== 登山路径规划 ===");
        System.out.printf("地形节点数: %d%n", mountain.nodes.size());

        // 寻找山脚(海拔最低)和山顶(海拔最高)
        int startId = 0, goalId = 0;
        double minElev = Double.MAX_VALUE, maxElev = -Double.MAX_VALUE;
        for (Node node : mountain.nodes) {
            if (node.elevation < minElev) {
                minElev = node.elevation;
                startId = node.id;
            }
            if (node.elevation > maxElev) {
                maxElev = node.elevation;
                goalId = node.id;
            }
        }

        System.out.printf("山脚节点: #%d (海拔%.1fm)%n", startId, minElev);
        System.out.printf("山顶节点: #%d (海拔%.1fm)%n%n", goalId, maxElev);

        // 求解最优路径
        PathFinder finder = new PathFinder();
        Result result = finder.findOptimalPath(mountain, startId, goalId);

        System.out.printf("最优路径节点数: %d%n", result.path.size());
        System.out.printf("单位质量能量消耗: %.2f J/kg%n", result.totalEnergy);
        System.out.printf("70kg登山者总能量: %.2f kJ%n", result.totalEnergy * 70 / 1000);

        System.out.println("\n路径详情:");
        double totalDist = 0;
        for (int i = 0; i < result.path.size(); i++) {
            Node node = mountain.nodes.get(result.path.get(i));
            System.out.printf("  [%d] 节点#%d (%.1f, %.1f, 海拔%.1fm)%n",
                    i, node.id, node.x, node.y, node.elevation);
            if (i > 0) {
                Node prev = mountain.nodes.get(result.path.get(i - 1));
                totalDist += prev.distanceTo(node);
            }
        }
        System.out.printf("\n总路程: %.2f米%n", totalDist);
    }
}

四、算法复杂度分析

  • 建图:O(V + E),V 为节点数,E 为边数
  • Dijkstra算法:O(E log V),使用优先队列实现
  • 路径重建:O(V)

五、扩展思考

  1. 多目标优化:同时优化能量消耗、时间和风景评分,使用帕累托前沿求解
  2. 动态地形:考虑天气导致的地面湿滑系数变化,动态调整边权
  3. 团队登山:引入多人协作约束,如等待时间、绳索长度限制