【无标题】

物流串线场景下的全源最短路径算法------Johnson 算法原理与实现

一、问题背景:什么是物流串线

在物流调度系统中,串线(Route Crossing)指一个运单的货物从始发网点出发,经多个中转网点最终送达目的网点的全过程路径规划。一个典型的物流网络包含数十到数百个网点(枢纽),网点间通过干线运输相连,每条线路具有不同的运输成本(距离、时效、费用)。

核心需求

调度系统需要实时回答以下问题:

任意两个网点之间,成本最低的运输路线是什么?如果存在备选线路(中转),最优成本又是多少?

这本质上是一个 全源最短路径 (All-Pairs Shortest Path, APSP)问题------我们需要一次性计算出所有网点对之间的最短距离,然后供串线匹配、路径推荐、成本预估等下游模块查询。


二、物流网络建模

2.1 有向带权图

将物流网络建模为有向带权图 ( G = (V, E) ):

  • 顶点 ( V ):物流网点(分拨中心、中转站、末端站点)

  • 有向边 ( (u, v) ):从网点 ( u ) 到网点 ( v ) 的运输线路

  • 边权 ( w(u, v) ):运输成本,可以是距离、时效或综合费用

    复制代码
      ┌──────┐    5    ┌──────┐
      │  北京 │ ──────▶│  上海 │
      └──┬───┘         └──┬───┘
         │  ┌─────────────┘
         │8 │ -2  (返程补贴线路)
         ▼  ▼
      ┌──────┐    4    ┌──────┐
      │  武汉 │ ──────▶│  广州 │
      └──┬───┘         └──────┘
         │    ┌───────────┘
         │5   │1
         ▼    ▼
      ┌──────┐
      │  深圳 │
      └──────┘

2.2 负权边的现实意义

物流网络中确实可能出现负权边,例如:

  • 返程补贴:为降低空载率,部分线路给予运费折扣甚至补贴(表现为负成本)
  • 拼车优惠:两个运单合并运输时,边际成本为负
  • 时效折算:将提前到达的奖励折算为负成本

传统 Dijkstra 算法无法处理负权边(会陷入死循环或错误结果),这给算法选型带来了挑战。


三、算法选型:为什么选 Johnson

候选方案对比

算法 时间复杂度 负权边 稀疏图表现 适用场景
Floyd-Warshall ( O(V^3) ) 差(全矩阵运算) 稠密图 ( E \approx V^2 )
重复 Dijkstra ( O(V \cdot (E + V \log V)) ) 仅非负权图
Bellman-Ford 重复 ( O(V^2 E) ) 极小规模图
Johnson ( O(V E + V^2 \log V) ) 稀疏图 + 负权边

选择理由

物流网络是典型的稀疏图------每个网点通常只连接 3~8 个相邻网点,即 ( E \ll V^2 )。因此:

  • Floyd-Warshall 的 ( O(V^3) ) 对于 500 个网点意味着 1.25 亿次运算,严重浪费
  • Johnson 在稀疏图上的 ( O(VE + V^2 \log V) ) 约为 Floyd 的 ( 1/V ) 量级
  • Johnson 兼有负权边处理能力和稀疏图性能优势,是物流场景的最优解

四、Johnson 算法原理

Johnson 算法的核心思想是 「势能重赋权」(Potential Re-weighting),将含负权边的图魔法般地转成非负权图,然后放心跑 Dijkstra。

4.1 直观理解

想象我们给每个网定义一个「海拔高度」( h(v) ):

  • 从高处流向低处,重力做功 → 边权减少
  • 但对任意路径,起点和终点的海拔差固定,不影响路径间的相对排序

数学上,定义势能函数 ( h: V \rightarrow \mathbb{R} ),对每条边重赋权:

w'(u, v) = w(u, v) + h(u) - h(v)

关键是:对任意 ( u \rightarrow v ) 的路径 ( P ),重赋权后的路径长度与原路径长度仅差一个常数 ( h(u) - h(v) ),因此最短路径保持不变!

4.2 如何构造 h(v)

势能函数需要满足 ( w'(u, v) \ge 0 ),即:

w(u, v) + h(u) - h(v) \\ge 0 \\quad \\Rightarrow \\quad h(v) \\le h(u) + w(u, v)

这正是三角不等式!也就是说,( h(v) ) 就是从某个源点出发的最短路径距离。因此:

  1. 添加虚拟源点 ( s ),向所有顶点连权为 0 的边
  2. 对扩展图运行 Bellman-Ford,以 ( s ) 为起点求得 ( h(v) = \delta(s, v) )
  3. 此时 ( h(v) \le h(u) + w(u, v) ) 自动成立 → ( w'(u, v) \ge 0 )

4.3 四步全景

复制代码
Step 1: 添加虚拟源点        Step 2: Bellman-Ford求h(v)
    ┌───┐                        h(A)=0, h(B)=-2, h(C)=-1 ...
    │ s │──0──▶ A,B,C,D          │
    └───┘                        ▼
                          Step 3: 重赋权
                          w'(u,v)=w(u,v)+h(u)-h(v) ≥ 0
                                 │
                                 ▼
                          Step 4: 每个顶点跑Dijkstra
                          然后还原: d(u,v)=d'(u,v)-h(u)+h(v)

五、Java 实现详解

5.1 数据结构

java 复制代码
static class Edge {
    int to;      // 目标节点
    int weight;  // 边权(允许为负)
}
// 邻接表表示稀疏图
List<List<Edge>> graph;

5.2 Bellman-Ford(带提前终止优化)

java 复制代码
private static int[] bellmanFord(List<List<Edge>> graph, int s) {
    int n = graph.size();
    int[] dist = new int[n];
    Arrays.fill(dist, INF);
    dist[s] = 0;

    for (int i = 0; i < n - 1; i++) {
        boolean updated = false;          // ⭐ 优化:追踪是否有松弛发生
        for (int u = 0; u < n; u++) {
            if (dist[u] == INF) continue;
            for (Edge e : graph.get(u)) {
                if (dist[u] + e.weight < dist[e.to]) {
                    dist[e.to] = dist[u] + e.weight;
                    updated = true;
                }
            }
        }
        if (!updated) break;              // ⭐ 无松弛则提前退出
    }
    // 第 V 轮检测负权环
    for (int u = 0; u < n; u++) {
        if (dist[u] == INF) continue;
        for (Edge e : graph.get(u)) {
            if (dist[u] + e.weight < dist[e.to]) {
                return null;              // 存在负权环,算法终止
            }
        }
    }
    return dist;
}

提前终止优化:对于大多数实际物流网络(无负权环),Bellman-Ford 往往在 2~3 轮内收敛,加上此优化后实际开销远小于理论最坏 ( O(VE) )。

5.3 重赋权核心

java 复制代码
// 计算势能
int[] h = bellmanFord(augmented, n);

// 重赋权:w'(u,v) = w(u,v) + h(u) - h(v) ≥ 0
List<List<Edge>> reweighted = new ArrayList<>();
for (int u = 0; u < n; u++) {
    for (Edge e : graph.get(u)) {
        int newWeight = e.weight + h[u] - h[e.to];
        newEdges.add(new Edge(e.to, newWeight));
    }
}

为什么非负:因为 ( h(v) ) 是 Bellman-Ford 的最短距离,满足三角不等式 ( h(v) \le h(u) + w(u,v) ),移项即得 ( h(u) + w(u,v) - h(v) \ge 0 )。

5.4 最短路径还原

java 复制代码
// 对每个顶点运行 Dijkstra
for (int u = 0; u < n; u++) {
    int[] d = dijkstra(reweighted, u);   // 重赋权图上的最短路
    for (int v = 0; v < n; v++) {
        // 还原真实距离:d(u,v) = d'(u,v) - h(u) + h(v)
        dist[u][v] = d[v] - h[u] + h[v];
    }
}

还原公式推导:对任意路径 ( P: u \rightsquigarrow v ):

\\begin{aligned} w'§ \&= \\sum_{(x,y)\\in P} \[w(x,y) + h(x) - h(y)\] \\ \&= \\sum w(x,y) + h(u) - h(v) \\quad\\text{(中间项相互抵消)} \\ \&= w§ + h(u) - h(v) \\end{aligned}

所以 ( d'(u,v) = d(u,v) + h(u) - h(v) ),移项即得还原公式。


六、物流串线中的应用

6.1 离线预计算 + 在线查询

java 复制代码
// 1. 系统启动时:构建物流网络图
List<List<Edge>> logisticsGraph = buildLogisticsGraph();

// 2. 运行 Johnson 算法,预计算全源最短路径距离矩阵
int[][] apsp = Johnson.johnson(logisticsGraph);

// 3. 串线匹配时:O(1) 查询
int cost = apsp[originHub][destHub];  // 任意两网点间最优成本

6.2 串线推荐流程

复制代码
                     ┌─────────────────────┐
                     │  APSP 距离矩阵       │
                     │  (Johnson预计算)      │
                     └──────┬──────────────┘
                            │ O(1) 查询
                            ▼
运单A: 北京→深圳    ──▶  北京→深圳 最优成本 = C1
运单B: 上海→广州    ──▶  上海→广州 最优成本 = C2
                            │
                    ┌───────▼────────┐
                    │  串线匹配引擎    │
                    │  判断是否能合并   │
                    │  运输节省成本     │
                    └───────┬────────┘
                            ▼
                    推荐串线方案:
                    北京→上海(A货) + 上海→深圳(A货)
                    上海→广州(B货) + 广州→深圳(B货)

6.3 性能表现

以 500 个网点的物流网络为例(每个网点平均连接 5 条边):

阶段 运算量 实际耗时(估算)
Bellman-Ford 求势能 ( 500 \times 2500 ) 次松弛 ~5ms
V 次 Dijkstra ( 500 \times (2500 + 500\log 500) ) ~200ms
总计 --- ~205ms

对比 Floyd-Warshall 的 ( 500^3 = 1.25 ) 亿次 → 约 1200ms ,Johnson 快约 6 倍。网络规模越大,优势越明显。


七、工程优化要点

7.1 负权环检测

物流网络中不应存在负权环(否则意味着无限套利),Johnson 算法在 Bellman-Ford 阶段自动检测:

java 复制代码
if (h == null) {
    System.err.println("图中存在负权环!请检查线路数据");
    return null;
}

7.2 整数边权 vs 浮点边权

本实现使用 int 类型表示边权,适合以分为单位 的成本计算,避免浮点精度问题。如需小数支持,可改为 double,同时在松弛判断中使用 dist[u] + w < dist[v] - EPS

7.3 增量更新

网点/线路变化时,不需要完全重算 APSP 矩阵。可维护增量 Johnson:仅更新受影响的行列,将更新成本从 ( O(VE) ) 降低到 ( O(E + V\log V) )。


八、总结

Johnson 算法在物流串线场景下是一个「恰到好处」的选择:

  • 处理负权边:适应返程补贴、拼车优惠等真实业务场景
  • 稀疏图高效:利用邻接表 + 优先队列,时间复杂度优于 Floyd-Warshall
  • 一次性预计算:APSP 矩阵构建后支持 ( O(1) ) 实时查询
  • 负权环检测:自动发现数据异常,保障调度系统稳定性
java 复制代码
/**
 * Johnson's Algorithm ------ 全源最短路径算法
 * <p>
 * 支持含负权边的有向图,时间复杂度 O(V² log V + VE)
 * <p>
 * 核心步骤:
 * 1. 新增虚拟源点 s,向所有顶点连权为 0 的边
 * 2. 用 Bellman-Ford 从 s 求势能函数 h(v)
 * 3. 重赋权:w'(u,v) = w(u,v) + h(u) - h(v),使所有边权非负
 * 4. 对每个顶点运行 Dijkstra,再还原真实距离
 */
public class Johnson {

    /* ========== 1. 数据结构定义 ========== */
    static class Edge {
        int to;      // 目标节点
        int weight;  // 边权
        Edge(int to, int weight) {
            this.to = to;
            this.weight = weight;
        }
    }

    private static final int INF = Integer.MAX_VALUE;

    /* ========== 2. Bellman-Ford 求势能 ========== */

    /**
     * Bellman-Ford 算法:从虚拟源点出发计算势能函数 h(v)
     *
     * @param graph 邻接表(含虚拟源点)
     * @param s     虚拟源点编号
     * @return      势能数组 h[],若存在负权环则返回 null
     */
    private static int[] bellmanFord(List<List<Edge>> graph, int s) {
        int n = graph.size();
        int[] dist = new int[n];
        Arrays.fill(dist, INF);
        dist[s] = 0;

        // 松弛 V-1 轮(提前终止优化:若某轮无松弛则直接退出)
        for (int i = 0; i < n - 1; i++) {
            boolean updated = false;
            for (int u = 0; u < n; u++) {
                if (dist[u] == INF) continue;
                for (Edge e : graph.get(u)) {
                    if (dist[u] + e.weight < dist[e.to]) {
                        dist[e.to] = dist[u] + e.weight;
                        updated = true;
                    }
                }
            }
            if (!updated) break; // 无松弛,提前终止
        }

        // 检测负权环
        for (int u = 0; u < n; u++) {
            if (dist[u] == INF) continue;
            for (Edge e : graph.get(u)) {
                if (dist[u] + e.weight < dist[e.to]) {
                    return null; // 存在负权环
                }
            }
        }
        return dist;
    }

    /* ========== 3. Dijkstra 单源最短路 ========== */

    /**
     * 在非负权图上运行 Dijkstra 算法
     *
     * @param graph 重赋权后的邻接表
     * @param start 起点编号
     * @return      最短距离数组
     */
    private static int[] dijkstra(List<List<Edge>> graph, int start) {
        int n = graph.size();
        int[] dist = new int[n];
        Arrays.fill(dist, INF);
        dist[start] = 0;
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        pq.offer(new int[]{start, 0});
        boolean[] visited = new boolean[n];

        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int u = cur[0];
            if (visited[u]) continue;
            visited[u] = true;
            for (Edge e : graph.get(u)) {
                int v = e.to;
                int w = e.weight;
                if (dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                    pq.offer(new int[]{v, dist[v]});
                }
            }
        }
        return dist;
    }

    /* ========== 4. Johnson 全源最短路 ========== */

    /**
     * Johnson's Algorithm:计算所有顶点对之间的最短路径
     *
     * @param graph 原始有向图邻接表(节点编号 0 ~ n-1,允许负权边)
     * @return      n×n 距离矩阵,dist[i][j] 为 i 到 j 的最短距离;
     *              若图中存在负权环则返回 null
     */
    public static int[][] johnson(List<List<Edge>> graph) {
        int n = graph.size();

        // ---- Step 1: 构建含虚拟源点 s = n 的新图 ----
        List<List<Edge>> augmented = new ArrayList<>(n + 1);
        for (int i = 0; i < n; i++) {
            augmented.add(new ArrayList<>(graph.get(i)));
        }
        augmented.add(new ArrayList<>()); // 虚拟源点 s
        for (int v = 0; v < n; v++) {
            augmented.get(n).add(new Edge(v, 0)); // s → v,权为 0
        }

        // ---- Step 2: Bellman-Ford 求势能 h(v) ----
        int[] h = bellmanFord(augmented, n);
        if (h == null) {
            System.err.println("图中存在负权环,Johnson 算法无法执行!");
            return null;
        }

        // ---- Step 3: 重赋权,使所有边权非负 ----
        List<List<Edge>> reweighted = new ArrayList<>(n);
        for (int u = 0; u < n; u++) {
            List<Edge> newEdges = new ArrayList<>();
            for (Edge e : graph.get(u)) {
                // w'(u,v) = w(u,v) + h(u) - h(v) >= 0
                newEdges.add(new Edge(e.to, e.weight + h[u] - h[e.to]));
            }
            reweighted.add(newEdges);
        }

        // ---- Step 4: 对每个顶点运行 Dijkstra,并还原真实距离 ----
        int[][] dist = new int[n][n];
        for (int u = 0; u < n; u++) {
            int[] d = dijkstra(reweighted, u);
            for (int v = 0; v < n; v++) {
                if (d[v] == INF) {
                    dist[u][v] = INF;
                } else {
                    // 还原:d(u,v) = d'(u,v) - h(u) + h(v)
                    dist[u][v] = d[v] - h[u] + h[v];
                }
            }
        }
        return dist;
    }

    /* ========== 5. 有向图加边辅助方法 ========== */

    /**
     * 有向图加边
     */
    public static void addEdge(List<List<Edge>> g, int u, int v, int w) {
        g.get(u).add(new Edge(v, w));
    }

    /* ========== 6. 示例 & 测试 ========== */

    public static void main(String[] args) {
        int n = 4; // 节点数 0~3
        List<List<Edge>> graph = new ArrayList<>(n);
        for (int i = 0; i < n; i++) graph.add(new ArrayList<>());

        // 构建含负权边的有向图
        addEdge(graph, 0, 1, 3);
        addEdge(graph, 0, 2, 8);
        addEdge(graph, 1, 2, -2);
        addEdge(graph, 1, 3, 1);
        addEdge(graph, 2, 0, 5);
        addEdge(graph, 2, 3, 4);
        addEdge(graph, 3, 0, -1);

        int[][] dist = johnson(graph);
        if (dist == null) return;

        // 打印全源最短路径距离矩阵
        System.out.println("全源最短路径距离矩阵:");
        System.out.print("    ");
        for (int j = 0; j < n; j++) System.out.printf("%6d", j);
        System.out.println();
        for (int i = 0; i < n; i++) {
            System.out.printf("%2d: ", i);
            for (int j = 0; j < n; j++) {
                if (dist[i][j] == INF) {
                    System.out.print("   INF");
                } else {
                    System.out.printf("%6d", dist[i][j]);
                }
            }
            System.out.println();
        }
    }
}```

---

*本文所述算法实现基于 Apache 2.0 开源协议,欢迎在物流调度、路径规划等场景中引用和改进。*
相关推荐
Smilecoc1 小时前
决策树(四):决策树实战之鸢尾花分类
算法·决策树·分类
数据仓库搬砖人1 小时前
DBSCAN 原理深度解析:从聚类算法到风控团伙识别的实战指南
算法
王五周八1 小时前
Tesseract OCR的Java使用(附安装包,非常详细)
java·开发语言·ocr
旧书包的青春1 小时前
2026年6月11日
java
一直奔跑在路上1 小时前
深入浅出RDMA:原理、应用与实战指南
开发语言·php
凡人叶枫1 小时前
Effective C++ 条款24:若所有参数皆须要类型转换,请为此采用 non-member 函数
linux·前端·c++·算法·嵌入式开发
洛水水1 小时前
【力扣100题】87.只出现一次的数字
数据结构·算法·leetcode
HZ·湘怡1 小时前
排序算法之希尔排序(2)--菜鸟先飞
数据结构·算法·排序算法·希尔排序
乐观勇敢坚强的老彭1 小时前
2026全国青少年信息素养大赛(Python小学组)复赛复习讲义
python·算法·数学建模