文章目录
最短路之前也有写博客讲解,此处再讲一遍
戳这里
Dijkstra 算法
在计算机科学和图论的领域中,Dijkstra 算法是一种用于求解图中最短路径问题的经典算法。在这篇博客中,我们将深入探讨 Dijkstra 算法的原理、实现以及其广泛的应用。
算法的原理
Dijkstra 算法的基本思想是从起始顶点开始,逐步扩展到其他顶点,每次选择距离起始顶点最近的未访问顶点,并更新与其相邻顶点的距离。通过不断重复这个过程,最终可以得到起始顶点到图中所有其他顶点的最短路径。
为了实现这个过程,Dijkstra 算法使用了一个距离数组来记录起始顶点到每个顶点的当前最短距离,以及一个标记数组来标记哪些顶点已经被访问过。
实现步骤
- 初始化:将起始顶点到自身的距离设置为 0,到其他顶点的距离设置为无穷大。将所有顶点标记为未访问。
- 选择当前距离起始顶点最近的未访问顶点。
- 对于所选顶点的相邻顶点,更新它们到起始顶点的距离,如果新的距离更短,则更新距离数组。
- 标记所选顶点为已访问。
- 重复步骤 2 - 4,直到所有顶点都被访问。
以下是 Dijkstra 算法的 C++ 实现示例:
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
const int INF = std::numeric_limits<int>::max();
struct Edge {
int destination;
int weight;
};
void dijkstra(std::vector<std::vector<Edge>>& graph, int source) {
int n = graph.size();
std::vector<int> distance(n, INF);
std::vector<bool> visited(n, false);
distance[source] = 0;
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
pq.push({0, source});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (visited[u]) {
continue;
}
visited[u] = true;
for (const Edge& edge : graph[u]) {
int v = edge.destination;
int weight = edge.weight;
if (distance[u] + weight < distance[v]) {
distance[v] = distance[u] + weight;
pq.push({distance[v], v});
}
}
}
// 打印结果
std::cout << "从顶点 " << source << " 到其他顶点的最短距离: " << std::endl;
for (int i = 0; i < n; ++i) {
std::cout << "到顶点 " << i << " 的距离: " << (distance[i] == INF? "无穷大" : std::to_string(distance[i])) << std::endl;
}
}
int main() {
std::vector<std::vector<Edge>> graph = {
{{1, 4}, {2, 2}},
{{3, 5}, {4, 1}},
{{4, 3}},
{{5, 2}},
{{5, 3}},
{}
};
int source = 0;
dijkstra(graph, source);
return 0;
}
应用场景
-
交通网络:在导航系统中,为用户找到从起点到终点的最短路径。
- 例如,在地图应用中,为司机规划最短的行驶路线,以节省时间和燃料。
-
通信网络:确定数据在网络中传输的最优路径,以减少延迟和提高效率。
-
物流配送:规划货物运输的最短路线,降低成本。
时间复杂度分析
Dijkstra 算法的时间复杂度主要取决于使用的数据结构。在上述实现中,使用了优先队列,时间复杂度为 O((V + E) log V)
,其中 V
是顶点的数量,E
是边的数量。
Floyd 算法
算法原理
Floyd 算法的核心思想是通过动态规划的方式,逐次考虑图中的每个顶点作为中间节点,更新任意两点之间的最短路径。
其基本步骤如下:
- 初始化一个二维数组
dist[][]
,用于存储图中各顶点之间的初始距离。如果两个顶点之间有边相连,距离为边的权值;如果没有边相连,距离为无穷大。 - 依次遍历图中的每个顶点
k
。 - 对于每对顶点
i
和j
,更新dist[i][j]
的值为min(dist[i][j], dist[i][k] + dist[k][j])
。
C++ 代码实现
以下是一个简单的 Floyd 算法的 C++ 实现示例:
#include <iostream>
#include <climits>
// 定义无穷大的值
const int INF = INT_MAX;
// Floyd 算法函数
void floyd(int n, int** graph, int** dist) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[i][j] = graph[i][j];
}
}
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dist[i][k]!= INF && dist[k][j]!= INF && dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
}
// 打印结果函数
void printSolution(int n, int** dist) {
std::cout << "以下是任意两点之间的最短距离矩阵:" << std::endl;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dist[i][j] == INF)
std::cout << "INF ";
else
std::cout << dist[i][j] << " ";
}
std::cout << std::endl;
}
}
int main() {
int n = 4; // 顶点数量
// 初始化图的邻接矩阵
int** graph = new int*[n];
for (int i = 0; i < n; i++) {
graph[i] = new int[n];
}
graph[0][0] = 0;
graph[0][1] = 5;
graph[0][2] = INF;
graph[0][3] = 10;
graph[1][0] = INF;
graph[1][1] = 0;
graph[1][2] = 3;
graph[1][3] = INF;
graph[2][0] = INF;
graph[2][1] = INF;
graph[2][2] = 0;
graph[2][3] = 1;
graph[3][0] = INF;
graph[3][1] = INF;
graph[3][2] = INF;
graph[3][3] = 0;
// 用于存储最短距离的矩阵
int** dist = new int*[n];
for (int i = 0; i < n; i++) {
dist[i] = new int[n];
}
floyd(n, graph, dist);
printSolution(n, dist);
// 释放内存
for (int i = 0; i < n; i++) {
delete[] graph[i];
delete[] dist[i];
}
delete[] graph;
delete[] dist;
return 0;
}
代码分析
在上述代码中,我们首先定义了一个常量 INF
表示无穷大。
floyd
函数实现了 Floyd 算法的核心逻辑。通过三层循环,依次考虑每个中间顶点来更新最短距离。
printSolution
函数用于打印最终计算得到的任意两点之间的最短距离矩阵。
在 main
函数中,我们初始化了一个示例的图的邻接矩阵,调用 floyd
函数计算最短距离,然后打印结果,并释放动态分配的内存。
应用场景
Floyd 算法在许多领域都有广泛的应用,例如:
- 交通规划:帮助确定城市中不同地点之间的最短行车路线。
- 网络路由:优化数据在网络节点之间的传输路径。
Bellman-Ford 算法
算法原理
Bellman-Ford 算法的核心思想是通过对图中的边进行多次松弛操作,逐步更新源点到各个顶点的最短距离估计值。其基本步骤如下:
- 初始化:将源点到所有其他顶点的距离初始化为无穷大(用一个极大值表示),源点到自身的距离初始化为 0 。
- 松弛操作:对图中的每条边进行多次遍历。对于边
(u, v)
,其权值为w
,如果当前源点到u
的距离加上w
小于源点到v
的当前距离,就更新源点到v
的距离。 - 重复松弛操作,直到经过
V - 1
次遍历后没有距离被更新,其中V
是图中顶点的数量。
代码实现
#include <iostream>
#include <climits>
// 定义图的顶点数量
#define V 5
// 松弛函数
void relax(int u, int v, int w, int dist[], int parent[]) {
if (dist[u]!= INT_MAX && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
parent[v] = u;
}
}
// Bellman-Ford 算法函数
bool bellmanFord(int graph[V][V], int src) {
int dist[V];
int parent[V];
// 初始化距离和父节点
for (int i = 0; i < V; i++) {
dist[i] = INT_MAX;
parent[i] = -1;
}
dist[src] = 0;
// 进行 V - 1 次松弛操作
for (int i = 0; i < V - 1; i++) {
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
if (graph[u][v]!= 0) {
relax(u, v, graph[u][v], dist, parent);
}
}
}
}
// 检查是否存在负权环
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
if (graph[u][v]!= 0 && dist[u]!= INT_MAX && dist[u] + graph[u][v] < dist[v]) {
return false; // 存在负权环
}
}
}
// 打印结果
std::cout << "顶点\t距离\t父节点" << std::endl;
for (int i = 0; i < V; i++) {
std::cout << i << "\t" << dist[i] << "\t" << parent[i] << std::endl;
}
return true; // 不存在负权环
}
int main() {
int graph[V][V] = {
{0, -1, 4, 0, 0},
{0, 0, 3, 2, 2},
{0, 0, 0, 0, 0},
{0, 1, 5, 0, 0},
{0, 0, 0, -3, 0}
};
int source = 0;
if (bellmanFord(graph, source)) {
std::cout << "不存在负权环,最短路径计算成功" << std::endl;
} else {
std::cout << "存在负权环" << std::endl;
}
return 0;
}
在上述代码中,我们首先定义了一个松弛函数 relax
,用于更新顶点之间的距离和父节点。然后,bellmanFord
函数实现了 Bellman-Ford 算法的主要逻辑,包括初始化、多次松弛操作和负权环的检测。
算法分析
- 时间复杂度:Bellman-Ford 算法的时间复杂度为
O(VE)
,其中V
是顶点数量,E
是边的数量。这是因为需要对每条边进行V - 1
次松弛操作。 - 空间复杂度:主要取决于存储图的方式和存储距离、父节点等信息的数组,空间复杂度为
O(V)
。
应用场景
- 网络路由:在网络拓扑中计算最短路径,处理可能存在负权的链路。
- 金融分析:例如计算在具有不同收益和成本的投资组合中的最优路径。
SPFA 算法
在图论的广阔天地中,寻找最短路径的算法层出不穷,SPFA(Shortest Path Faster Algorithm)算法便是其中一颗耀眼的明星。今天,让我们一同深入探索 SPFA 算法,并通过 C++ 代码来实现它。
算法原理
SPFA 算法是对 Bellman-Ford 算法的一种优化改进。其核心思想是通过维护一个队列,动态地对顶点进行松弛操作,从而逐步找到从源点到其他顶点的最短路径。
与其他常见的最短路径算法(如 Dijkstra 算法)相比,SPFA 算法的独特之处在于它能够有效地处理带有负权边的图。
工作流程
-
初始化阶段:
- 将源点到自身的距离初始化为 0,其他顶点的距离初始化为无穷大。
- 将源点放入一个队列中。
-
循环处理阶段:
- 取出队列头部的顶点。
- 对该顶点的所有邻接顶点进行松弛操作:如果通过当前顶点能够缩短到邻接顶点的距离,就更新邻接顶点的距离,并在邻接顶点不在队列中的情况下将其入队。
- 重复这个过程,直到队列为空。
算法优势
- 对负权边的处理能力:这使得它在许多实际场景中更具适用性,因为现实中的问题可能存在负权情况。
- 通常情况下的高效性:在大多数情况下,其性能优于传统的 Bellman-Ford 算法。
C++ 实现
以下是一个完整的 SPFA 算法的 C++ 实现示例:
#include <iostream>
#include <vector>
#include <queue>
const int INF = 1e9; // 表示无穷大
// 边的结构体
struct Edge {
int to;
int weight;
};
// SPFA 算法函数
bool spfa(const std::vector<std::vector<Edge>>& graph, int source, std::vector<int>& distance) {
int n = graph.size();
std::vector<bool> inQueue(n, false); // 标记顶点是否在队列中
std::queue<int> q;
distance.assign(n, INF); // 初始化距离为无穷大
distance[source] = 0; // 源点距离为 0
q.push(source);
inQueue[source] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
inQueue[u] = false;
for (const Edge& edge : graph[u]) {
int v = edge.to;
int w = edge.weight;
if (distance[u] + w < distance[v]) {
distance[v] = distance[u] + w;
if (!inQueue[v]) {
q.push(v);
inQueue[v] = true;
}
}
}
}
// 检查是否存在负环
for (const auto& edges : graph) {
for (const Edge& edge : edges) {
int u = edge.to;
int v = edge.weight;
if (distance[u] + v < distance[v]) {
return false; // 存在负环
}
}
}
return true; // 不存在负环
}
int main() {
std::vector<std::vector<Edge>> graph = {
{{1, 1}, {2, 4}},
{{2, -1}},
{{3, 2}},
{}
};
int source = 0;
std::vector<int> distance;
if (spfa(graph, source, distance)) {
std::cout << "从源点 " << source << " 到其他顶点的最短距离: " << std::endl;
for (int i = 0; i < distance.size(); ++i) {
std::cout << "到顶点 " << i << " : " << (distance[i] == INF? "无穷大" : std::to_string(distance[i])) << std::endl;
}
} else {
std::cout << "图中存在负环" << std::endl;
}
return 0;
}
应用场景
- 交通运输领域:在物流运输中,考虑路况变化(如临时交通管制导致负权)来规划最优路线。
- 通信网络优化:当网络中存在信号增强设备(可视为负权)时,优化数据传输路径。
- 经济模型分析:例如在供应链中,考虑成本补贴等因素,找到成本最低的路径。
- 资源分配问题:对于存在资源回收或再利用(可产生负权效果)的情况,进行合理的资源分配。
复杂度分析
-
时间复杂度:
- 平均情况:通常为
O(km)
,其中k
是一个较小的常数,m
是边的数量。 - 最坏情况:可能达到
O(nm)
,其中n
是顶点的数量。这种最坏情况在一些特殊的图结构中可能出现,但在实际应用中较为少见。
- 平均情况:通常为
-
空间复杂度:主要取决于存储图的结构和辅助数据结构(如队列、距离数组等),通常为
O(n + m)
。
Johnson 算法
在 C++ 的算法领域中,Johnson 算法是一个令人瞩目的存在,它为解决图论中所有节点对之间的最短路径问题提供了一种有效的方法。
基本原理
Johnson 算法的核心在于通过巧妙的预处理和后续的计算,得出图中所有节点对之间的最短路径。其基本步骤如下:
- 首先,对原始图添加一个新的虚拟节点,并将其与所有其他节点相连,边的权重设为 0。
- 然后,使用 Bellman-Ford 算法从这个虚拟节点计算出所有节点的最短路径权重调整值。
- 接下来,基于调整后的权重,使用 Dijkstra 算法为每个节点计算到其他节点的最短路径。
实现方式
在 C++ 实现 Johnson 算法时,通常会用到以下数据结构:
- 邻接表或邻接矩阵来表示图的结构。邻接表适合稀疏图,能节省存储空间;邻接矩阵则适用于稠密图,操作相对简单。
- 优先队列(如
priority_queue
)用于 Dijkstra 算法中,以便快速找到当前最短路径的节点。
主要步骤
以下是一个简单的 Johnson 算法 C++ 实现的主要框架:
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
// 表示图的边
struct Edge {
int destination;
int weight;
};
// 计算单源最短路径的 Dijkstra 算法
void dijkstra(std::vector<std::vector<Edge>>& graph, int source, std::vector<int>& distance) {
int n = graph.size();
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
distance.assign(n, std::numeric_limits<int>::max());
distance[source] = 0;
pq.push({0, source});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
for (const auto& edge : graph[u]) {
int v = edge.destination;
int weight = edge.weight;
if (distance[u] + weight < distance[v]) {
distance[v] = distance[u] + weight;
pq.push({distance[v], v});
}
}
}
}
// Johnson 算法的主体
void johnsonAlgorithm(std::vector<std::vector<Edge>>& graph) {
int n = graph.size();
// 添加虚拟节点
graph.push_back(std::vector<Edge>());
for (int i = 0; i < n; ++i) {
graph[n].push_back({i, 0});
}
std::vector<int> potential(n, 0);
// 使用 Bellman-Ford 计算潜在距离
bool hasNegativeCycle = false;
for (int i = 0; i < n; ++i) {
if (bellmanFord(graph, n, potential, i)) {
hasNegativeCycle = true;
break;
}
}
if (hasNegativeCycle) {
std::cout << "图中存在负权环" << std::endl;
return;
}
// 重新调整权重
std::vector<std::vector<Edge>> reweightedGraph = graph;
for (int u = 0; u < n; ++u) {
for (auto& edge : reweightedGraph[u]) {
edge.weight += potential[u] - potential[edge.destination];
}
}
std::vector<std::vector<int>> distances(n, std::vector<int>(n));
for (int i = 0; i < n; ++i) {
dijkstra(reweightedGraph, i, distances[i]);
}
// 输出结果
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (distances[i][j]!= std::numeric_limits<int>::max()) {
distances[i][j] += potential[j] - potential[i];
std::cout << "从节点 " << i << " 到节点 " << j << " 的最短距离为: " << distances[i][j] << std::endl;
} else {
std::cout << "从节点 " << i << " 到节点 " << j << " 不存在路径" << std::endl;
}
}
}
}
int main() {
// 示例图的构建
std::vector<std::vector<Edge>> graph = {
{{1, 3}, {2, 8}},
{{2, 4}, {3, -5}},
{{3, 2}},
{}
};
johnsonAlgorithm(graph);
return 0;
}
应用场景
Johnson 算法在许多实际场景中都有重要的应用,比如:
- 交通网络规划:帮助确定城市中不同地点之间的最优行驶路线。
- 物流配送优化:找到货物从仓库到各个配送点的最短路径,降低运输成本。
优势与局限性
Johnson 算法的优势在于它能够有效地处理包含负权边的图,并且在时间复杂度上通常表现良好。然而,它也有一定的局限性,例如对于大规模的图,其空间复杂度可能较高。
这是我的第二十五篇文章,如有纰漏也请各位大佬指正
辛苦创作不易,还望看官点赞收藏打赏,后续还会更新新的内容。