每日算法 — 使用java实现青蛙过河:Dijkstra最短路径与动态风险权重

引言

青蛙过河是一个经典的智力游戏:河面上分布着若干石头,青蛙需要从起始石头跳跃到对岸,每次跳跃受限于青蛙的最大跳跃距离。然而,石头可能沉没、表面湿滑程度不同,使得每次跳跃都伴随着”风险代价”。如何将”最小化总风险”作为目标,找到最优过河路径?这正是Dijkstra最短路径算法在非负权图上的典型应用。本文用Java实现带风险权重的青蛙过河问题,讲解Dijkstra算法、优先队列优化与动态风险权重建模。

问题建模

将河面抽象为一个带权无向图:
– 节点:每块石头(含起始岸和对岸)
– 边:若两块石头的距离不超过青蛙最大跳跃距离,则存在一条边
– 边权:不仅考虑欧氏距离,还引入动态风险权重(如石头湿滑度、稳定性、水流速度)
– 目标:从起点到终点的总风险代价最小

风险权重函数设计:

risk(u, v) = distance(u, v) * (1 + slip_factor[v]) / stability[v]

其中 slip_factor 表示湿滑系数,stability 表示石头稳定性(0~1)。

核心算法:Dijkstra与优先队列优化

算法思路

  1. 图的构建:根据石头坐标和青蛙最大跳跃距离构建邻接表
  2. 风险权重计算:为每条边计算动态风险权重
  3. Dijkstra执行:
  4. 初始化:起点风险为0,其余为无穷大
  5. 优先队列(最小堆)按当前累计风险排序
  6. 每次取出风险最小的节点,松弛其邻接边
  7. 重复直到终点出队或队列为空
  8. 路径回溯:通过 prev[] 数组记录前驱节点,输出最优路径

复杂度分析

  • 时间复杂度:O((V + E) log V),其中 V 为石头数,E 为边数。优先队列优化后每次取最小值为 O(log V)
  • 空间复杂度:O(V + E),邻接表与距离数组

Java完整实现

import java.util.*;

/**
 * 青蛙过河:Dijkstra最短路径与动态风险权重
 * 
 * 核心思路:
 * 1. 将河面石头建模为图节点,可跳跃关系建模为边
 * 2. 边权引入动态风险(距离×湿滑系数÷稳定性)
 * 3. 使用Dijkstra+优先队列求解起点到终点的最小风险路径
 * 4. 支持路径回溯与前驱记录
 */
public class FrogRiverCrossing {

    // ============ 石头节点类 ============
    static class Stone {
        int id;
        double x, y;           // 坐标位置
        double slipFactor;     // 湿滑系数 [0, 1],越大越滑
        double stability;      // 稳定性 (0, 1],越大越稳
        boolean isStart;       // 是否为起始岸
        boolean isEnd;         // 是否为对岸

        public Stone(int id, double x, double y, double slipFactor, double stability) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.slipFactor = Math.max(0, Math.min(1, slipFactor));
            this.stability = Math.max(0.01, Math.min(1, stability));
        }

        /** 计算到另一块石头的欧氏距离 */
        public double distanceTo(Stone other) {
            double dx = this.x - other.x;
            double dy = this.y - other.y;
            return Math.sqrt(dx * dx + dy * dy);
        }

        /** 计算跳跃风险权重 */
        public double riskTo(Stone other) {
            double dist = distanceTo(other);
            // 风险 = 距离 × (1 + 目标石头湿滑系数) / 目标石头稳定性
            double risk = dist * (1 + other.slipFactor) / other.stability;
            return risk;
        }

        @Override
        public String toString() {
            return String.format("Stone[%d](%.1f,%.1f) slip=%.2f stable=%.2f", 
                id, x, y, slipFactor, stability);
        }
    }

    // ============ 图的边类 ============
    static class Edge {
        int to;           // 目标节点
        double weight;    // 风险权重

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

    // ============ 优先队列元素 ============
    static class NodeState implements Comparable<NodeState> {
        int nodeId;
        double dist;      // 从起点到当前节点的累计风险

        public NodeState(int nodeId, double dist) {
            this.nodeId = nodeId;
            this.dist = dist;
        }

        @Override
        public int compareTo(NodeState o) {
            return Double.compare(this.dist, o.dist);
        }
    }

    // ============ 游戏核心 ============
    private List<Stone> stones;
    private double maxJump;   // 青蛙最大跳跃距离
    private List<List<Edge>> graph;

    public FrogRiverCrossing(double maxJump) {
        this.stones = new ArrayList<>();
        this.maxJump = maxJump;
    }

    public void addStone(Stone s) {
        stones.add(s);
    }

    /** 根据石头位置和最大跳跃距离构建邻接表 */
    public void buildGraph() {
        int n = stones.size();
        graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                Stone a = stones.get(i);
                Stone b = stones.get(j);
                double dist = a.distanceTo(b);
                if (dist <= maxJump) {
                    // 无向图:双向添加边,权重使用目标节点的风险属性
                    double riskAB = a.riskTo(b);
                    double riskBA = b.riskTo(a);
                    graph.get(i).add(new Edge(j, riskAB));
                    graph.get(j).add(new Edge(i, riskBA));
                }
            }
        }
    }

    /**
     * Dijkstra算法求解最小风险路径
     * @param startId 起始石头编号
     * @param endId   目标石头编号
     * @return 包含最短距离和路径的结果
     */
    public PathResult dijkstra(int startId, int endId) {
        int n = stones.size();
        double[] dist = new double[n];
        int[] prev = new int[n];
        boolean[] visited = new boolean[n];
        Arrays.fill(dist, Double.POSITIVE_INFINITY);
        Arrays.fill(prev, -1);

        PriorityQueue<NodeState> pq = new PriorityQueue<>();
        dist[startId] = 0;
        pq.offer(new NodeState(startId, 0));

        while (!pq.isEmpty()) {
            NodeState cur = pq.poll();
            int u = cur.nodeId;

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

            if (u == endId) {
                break; // 终点已确定最短距离
            }

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

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

        // 回溯路径
        List<Stone> path = new ArrayList<>();
        if (dist[endId] < Double.POSITIVE_INFINITY) {
            int at = endId;
            while (at != -1) {
                path.add(stones.get(at));
                at = prev[at];
            }
            Collections.reverse(path);
        }

        return new PathResult(dist[endId], path, prev);
    }

    /** 获取指定节点的所有邻接节点(用于调试) */
    public void printGraph() {
        for (int i = 0; i < graph.size(); i++) {
            System.out.print(stones.get(i) + " -> ");
            for (Edge e : graph.get(i)) {
                System.out.printf("[%d w=%.2f] ", e.to, e.weight);
            }
            System.out.println();
        }
    }

    // ============ 结果类 ============
    static class PathResult {
        double totalRisk;
        List<Stone> path;
        int[] prev;

        public PathResult(double totalRisk, List<Stone> path, int[] prev) {
            this.totalRisk = totalRisk;
            this.path = path;
            this.prev = prev;
        }

        @Override
        public String toString() {
            if (path.isEmpty()) {
                return "无法到达终点!";
            }
            StringBuilder sb = new StringBuilder();
            sb.append(String.format("最小总风险: %.4f\n最优路径 (%d 步):\n", totalRisk, path.size() - 1));
            for (int i = 0; i < path.size(); i++) {
                sb.append("  ").append(path.get(i));
                if (i < path.size() - 1) {
                    double stepRisk = path.get(i).riskTo(path.get(i + 1));
                    sb.append(String.format(" --[risk=%.2f]--> ", stepRisk));
                }
                sb.append("\n");
            }
            return sb.toString();
        }
    }

    // ============ 主程序 ============
    public static void main(String[] args) {
        // 场景:河宽100米,青蛙最大跳25米
        FrogRiverCrossing game = new FrogRiverCrossing(25.0);

        // 起始岸 (0, 0)
        Stone start = new Stone(0, 0, 0, 0, 1.0);
        start.isStart = true;
        game.addStone(start);

        // 随机生成河中的石头
        Random rand = new Random(123);
        int stoneCount = 15;
        for (int i = 1; i <= stoneCount; i++) {
            double x = 5 + rand.nextDouble() * 90;   // x: 5~95
            double y = -20 + rand.nextDouble() * 40; // y: -20~20,模拟河的宽度方向
            double slip = rand.nextDouble() * 0.8;   // 湿滑度 0~0.8
            double stable = 0.3 + rand.nextDouble() * 0.7; // 稳定性 0.3~1.0
            game.addStone(new Stone(i, x, y, slip, stable));
        }

        // 对岸 (100, 0)
        Stone end = new Stone(stoneCount + 1, 100, 0, 0, 1.0);
        end.isEnd = true;
        game.addStone(end);

        // 建图并求解
        game.buildGraph();
        System.out.println("=== 青蛙过河:Dijkstra最小风险路径 ===\n");
        System.out.println("河面石头分布:");
        for (Stone s : game.stones) {
            System.out.println("  " + s);
        }

        PathResult result = game.dijkstra(0, game.stones.size() - 1);
        System.out.println("\n" + result);

        // 对比:如果忽略风险权重,只按距离计算
        System.out.println("\n【对比】若仅按欧氏距离最短(不考风险):");
        // 此时所有边权为distance,Dijkstra退化为普通最短路
        // 为简化,输出实际跳跃距离之和
        if (!result.path.isEmpty()) {
            double totalDist = 0;
            for (int i = 0; i < result.path.size() - 1; i++) {
                totalDist += result.path.get(i).distanceTo(result.path.get(i + 1));
            }
            System.out.printf("路径总距离: %.2f 米\n", totalDist);
        }
    }
}

关键代码解析

1. 动态风险权重

public double riskTo(Stone other) {
    double dist = distanceTo(other);
    return dist * (1 + other.slipFactor) / other.stability;
}

风险函数将物理距离、环境属性(湿滑度)和结构属性(稳定性)融合为单一代价指标。这种设计使得Dijkstra算法在求解时自动”避开”湿滑且不稳定的石头,即使它们物理距离较近。

2. 优先队列优化

PriorityQueue<NodeState> pq = new PriorityQueue<>();

Java的 PriorityQueue 基于二叉堆实现,提供 O(log V) 的插入和删除最小值操作。相比普通数组找最小值的 O(V),整体复杂度从 O(V²) 优化到 O((V + E) log V),在稀疏图(河面石头分布稀疏)上优势显著。

3. 提前终止

if (u == endId) break;

Dijkstra算法保证当目标节点第一次从优先队列中取出时,其距离值已经是最短的。因此可以立即终止搜索,无需遍历全图。

扩展思考

  1. 负权边处理:若引入”顺流加速”等负权因素,Dijkstra不再适用,需改用Bellman-Ford或SPFA算法
  2. 多目标优化:同时优化风险与跳跃次数,可引入帕累托前沿或加权求和策略
  3. 实时动态图:石头随时间沉没(边删除)或新石头浮起(边插入),可用动态图最短路算法维护

总结

本文通过青蛙过河游戏,将Dijkstra最短路径算法与动态风险建模相结合。核心贡献在于:
– 设计了融合距离、湿滑度和稳定性的风险权重函数
– 使用优先队列将Dijkstra复杂度优化至 O((V + E) log V)
– 提供了完整的路径回溯与前驱记录机制

该框架可直接迁移至导航路线规划、网络路由优化、机器人路径规划等实际领域。