目录
前言
A.建议:
1.学习算法最重要的是理解算法的每一步,而不是记住算法。
2.建议读者学习算法的时候,自己手动一步一步地运行算法。
B.简介:
Johnson算法是图论中用于求解有向带权图中所有节点对之间的最短路径问题的一种有效方法,特别是在图中存在负权重边但没有负权重环的情况下。
一 代码实现
#include <stdio.h>
#include <limits.h>
// 假设我们有一个结构体来表示图的边
typedef struct Edge {
int from, to;
int weight; // 权重
} Edge;
// 初始化 Bellman-Ford 需要的数据结构,例如:dist数组记录从源点到各个节点的距离
void init_distances(int n, int dist[]) {
for (int i = 0; i <= n; ++i) {
dist[i] = INT_MAX;
}
}
// Bellman-Ford 算法实现
void bellman_ford(int n, Edge edges[], int m, int* dist) {
// ... 实现 Bellman-Ford 算法以计算从新加入的节点S到每个节点的最短路径
// dist[S] 初始化为0,其他初始化为无穷大
// 进行n-1轮松弛操作
// 检查是否存在负权重环
}
// 重新加权函数,给每条边加上一个偏移量以消除负权重的影响
void reweight_edges(int n, Edge edges[], int dist[], Edge newEdges[]) {
// ... 遍历所有边,并更新其权重为原权重加上 h[i] - h[j]
// 其中,h[] 是通过 Bellman-Ford 算法得到的从虚拟结点S到各结点的最短距离
// 注意:实际实现时需要处理新的无负权重图的存储
}
// 使用Dijkstra或其他适合无负权重图的最短路径算法(如SPFA)计算新图G'中各节点间的最短路径
// 这里假设已有一个基于优先队列的Dijkstra算法实现
void dijkstra(int start, Edge newEdges[], int n, int adjMatrix[][n], int dist[]) {
// ... 实现 Dijkstra 算法,使用新的加权后的图计算最短路径
}
// Johnson算法主流程
void johnson_algorithm(Edge originalEdges[], int n, int m) {
// 添加虚拟源节点S
int dist[n + 1];
// 使用Bellman-Ford算法计算从S出发的最短路径
bellman_ford(n + 1, originalEdges, m, dist);
// 创建一个新的边集用于重新加权
Edge newEdges[m];
reweight_edges(n, originalEdges, dist, newEdges);
// 对于每一对节点(i, j),使用Dijkstra算法计算从i到j的最短路径
for (int i = 0; i < n; ++i) {
dijkstra(i, newEdges, n, adjMatrix, dist);
// 在这里,dist[j]现在储存了从i到j的最短路径长度
}
}
// 主程序入口
int main() {
// ... 加载或生成图数据结构originalEdges和邻接矩阵adjMatrix
// 调用Johnson算法计算所有节点对的最短路径
johnson_algorithm(originalEdges, n, m);
return 0;
}
请注意,上述代码仅提供了算法的基本逻辑框架,具体实现细节(如邻接矩阵、邻接表的表示方式、Bellman-Ford和Dijkstra算法的具体实现等)需要根据实际项目需求填充完成。此外,在实际编程时还要考虑内存管理、错误检查(如检测负权重环的存在)以及其他工程实践上的优化措施。
二 时空复杂度
Johnson算法是一种结合了Bellman-Ford算法和Dijkstra算法的混合方法,用于求解带权有向图(允许存在负权重边但不允许负权重环)中所有节点对之间的最短路径。其时空复杂度可以分为两部分:
-
时间复杂度:
- Bellman-Ford阶段:首先使用Bellman-Ford算法从一个虚拟源点出发遍历整个图,以计算每个顶点到该源点的一个偏移量,使得通过这个偏移量调整后所有的边权重变成非负。Bellman-Ford的时间复杂度是,其中
V
是顶点数量,E
是边的数量。 - Dijkstra阶段:接下来,对于每个顶点都作为起点运行一次Dijkstra算法来找出经过重新加权后的图中的最短路径。由于Dijkstra在无负权重图上使用优先队列实现的时间复杂度为
,但在实际应用中,由于每个顶点只执行一次Dijkstra,所以总时间开销大约是。
因此,Johnson算法的整体时间复杂度大致是
,在稀疏图中(即
),时间复杂度接近于。
- Bellman-Ford阶段:首先使用Bellman-Ford算法从一个虚拟源点出发遍历整个图,以计算每个顶点到该源点的一个偏移量,使得通过这个偏移量调整后所有的边权重变成非负。Bellman-Ford的时间复杂度是,其中
-
空间复杂度:
- Johnson算法的空间复杂度主要由存储图结构、距离数组、以及优先队列所需的空间决定。
- 存储图结构通常需要
空间(如果采用邻接表)或(如果采用邻接矩阵)。
- 距离数组需要
空间。
- 对于Dijkstra算法,优先队列(如斐波那契堆)可能需要额外的
空间。
总体来说,空间复杂度通常是 或更高,取决于具体的数据结构选择和实现细节
三 优缺点
Johnson算法在图论中用于解决带权有向图(允许存在负权重边但不允许负权重环)中所有节点对之间的最短路径问题。以下是Johnson算法的主要优缺点:
A.优点:
- 处理负权重边:Johnson算法能够有效处理含有负权重边的图,而Floyd-Warshall算法在有负权重边时可能会得到错误的结果。
- 稀疏图性能好:对于稀疏图(即边的数量远小于顶点数量的平方),Johnson算法的时间复杂度优于Floyd-Warshall算法(
),其时间复杂度大致为
,当图是稀疏时效率更高。
- 多源最短路径:通过将问题转换为一系列单源最短路径问题,Johnson算法可以计算出任意两点间的最短路径。
B.缺点:
- 时间复杂度:尽管在稀疏图上表现较好,但在稠密图或者极度不平衡图中,该算法的时间复杂度可能较高,不适合大规模数据集。
- 空间复杂度:需要存储图结构、距离数组以及执行Dijkstra算法所需的优先队列等额外数据结构,导致较大的空间开销。
- 步骤复杂:Johnson算法涉及多个阶段,包括Bellman-Ford算法和多次Dijkstra算法的执行,增加了实现和理解的复杂性。
- 无法处理负权重环:虽然能处理负权重边,但如果图中存在负权重环,则无法正确运行,必须先通过其他方式检测并排除负权重环。
四 现实中的应用
Johnson算法在现实世界中有多种应用场景,尤其适用于需要计算带权有向图中任意两点间最短路径的问题。以下是一些实际应用的例子:
-
交通网络规划:
- 在设计和优化公共交通系统时,Johnson算法可用于寻找从一个站点到所有其他站点的最短路线,以提供最佳的换乘方案或确定车辆调度计划。
- 在公路网络中,可以用来计算城市间的最短行车距离,考虑道路的不同长度(权重)以及可能存在的过路费、拥堵等因素形成的负权重。
-
物流与配送系统:
- 物流公司能够利用Johnson算法来规划最优运输路线,特别是当运输成本(如时间、费用等)可以是正数也可以是负数(例如,某些路段在特定时间段内免费或者存在优惠)的情况下。
- 仓库内部的货物移动路径规划,通过赋予不同货架之间搬运的距离和时间不同的权重来找出效率最高的拣选路径。
-
社交网络分析:
- 在社交网络中,用户之间的互动关系可以用有向图表示,边的权重可以反映联系强度或信息传递的速度。Johnson算法可以用于发现用户对之间的最短影响路径,这在信息传播研究、病毒式营销等领域具有重要价值。
-
计算机网络:
- 在大型计算机网络中,数据包路由选择可以通过Johnson算法进行优化,尤其是在考虑网络拥塞、服务质量(QoS)等因素导致的数据传输延迟可正可负时。
-
企业行为监管:
- 如之前提及,Johnson算法可以应用于企业上网行为监管中,将员工或设备视为节点,交互频率或连接强度作为边的权重,从而发现潜在的异常通信模式或不合规行为路径。
-
旅行商问题(TSP) 的预处理阶段:
- 虽然Johnson算法本身不直接解决TSP问题,但在求解过程中,它可以作为一个预处理步骤来调整图的权重,以便后续使用其他算法(比如Christofides heuristic)时获得更好的近似解。