算法 - 图的实例 - 最短路径 (Shortest Path)
本文将介绍最短路径的基础知识,并用C++实现迪杰斯特拉算法(Dijkstra)和弗洛伊德算法(Floyed)。
在查看本文之前,需要一些数据结构和程序语言的基础。
尤其是“矩阵”、“矩阵的压缩(matrix)”、“图(graph)”等的知识。
文章目录
1 最短路径简述 (Introduction)
最短路径问题是找到一个顶点到另一个顶点之间权值之和最小的路径。
-
一个顶点称为源点
-
另一个顶点称为终点
其图是有向带权图。
2 迪杰斯特拉算法 (Dijkstra)
迪杰斯特拉算法 (Dijkstra) 适用于:
-
(1)源点只有一个;
-
(2)权值必须都是大于等于零。
2.1 算法基本思想 (Basic Idea)
Dijkstra算法意指按照路径长度的递增次序,逐步的产生最短路径。
-
(1)首先、求出长度最短的一条最短路径;
-
(2)其次,参照它求出长度次短的一条最短路径;
-
(3)最后,重复步骤(2)直到求出所有最短路径。
假设原图,起始点A
:
其从A开始到达各个点的权值,取最小值就是路径:
到达点\经过边数 | 1 | 2 | 3 |
---|---|---|---|
B | AB(10) | ||
C | ABC(60), ADC(50) | ||
D | AD(30) | ||
E | AE(90) | ADE(100) | ABCE(70), ADCE(60) |
F |
2.2 算法设计思路 (Design Idea)
其算法描述:
- 假设 n 个顶点, e 条边的图,求得最短路径的集合为 P, 顶点为 { v0, v1, … , vn }, 权值和的数组为 cost[n]。
- (1)初始化: P = { v0 } , cost[vadj] = Edge[v0][vadj] ;(adj为邻接顶点)
- (2)获取最短路径,并添加顶点: cost[vmin] = min(cost) , P = { v0, vmin } ;
- (3)更新路径长度与路径:cost[vminadj] = min(cost[vminadj], cost[vmin] + Edge[vmin][vminadj]) ;
- (4)重复步骤(2)和(3):直到所有顶点检测完成。
其 n 个顶点的图,时间复杂度 O(n2) 。
2.3 C++代码 (C++ Code)
// Author: https://blog.csdn.net/DarkRabbit
// Shortest Path
// 迪杰斯特拉算法
// params:
// graph: 需要计算的图
// begin: 起始顶点
// paths: 输出的顺序
// costs: 输出的权值和
// return:
// bool: 是否出错
bool Dujkstra(AMGraphInt* graph,
int begin,
std::vector<std::vector<int>>& paths,
std::vector<double>& costs)
{
if (graph == nullptr || !graph->IsOriented()) // 无向图返回
{
return false;
}
int size = graph->GetVertexCount(); // 顶点数量
if (begin < 0 || begin >= size) // 起始点不在图中,直接返回
{
return false;
}
double infinity = graph->GetDefaultWeight(); // 没有边的权值,即正无穷
paths.assign(size, std::vector<int>()); // 初始化空路径
costs.assign(size, infinity); // 初始化权值和
std::vector<bool> marks(size, false); // 顶点标记(集合)
// 起始顶点
costs[begin] = 0;
marks[begin] = true;
paths[begin].push_back(begin);
// 初始化costs,从起始点开始到达的首个顶点
double weight = 0;
for (int adj = graph->FirstNeighbor(begin);
adj != -1;
adj = graph->NextNeighbor(begin, adj))
{
graph->TryGetWeight(begin, adj, weight);
paths[adj].push_back(begin); // begin 加入到 adj 的路径
costs[adj] = weight;
}
// 循环,路径最多需要加入 size - 1 个顶点
for (int i = 1; i < size; i++)
{
int minVertex = -1; // 选出未访问过的,cost最小的顶点
double minCost = infinity; // 选出的最小cost
// 选出未访问过的,cost最小的顶点
for (int j = 0; j < size; j++)
{
// 如果顶点没有访问过,且cost更小
if (!marks[j] && costs[j] < minCost)
{
minVertex = j;
minCost = costs[j];
}
}
if (minVertex == -1) // 未选出顶点,说明其它顶点无法到达
{
break;
}
marks[minVertex] = true; // 访问此顶点
paths[minVertex].push_back(minVertex); // 加入路径
// 查找minVertex的邻接顶点,并更新路径和cost
for (int adj = graph->FirstNeighbor(minVertex);
adj != -1;
adj = graph->NextNeighbor(minVertex, adj))
{
graph->TryGetWeight(minVertex, adj, weight);
// 如果新cost更小,则更新,新cost = 到达上一个顶点的cost + 到达此顶点的 cost
if (!marks[adj] && costs[minVertex] + weight < costs[adj])
{
costs[adj] = costs[minVertex] + weight; // 更新cost
paths[adj].clear();
paths[adj].insert(paths[adj].begin(),
paths[minVertex].begin(),
paths[minVertex].end()); // 更新路径
}
}
}
return true;
}
3 弗洛伊德算法 (Floyed)
弗洛伊德算法 (Floyed) 相比较于 迪杰斯特拉算法 (Dijkstra) 它:
-
(1)可以有多个源点;
-
(2)权值可以是负数,但负数权值不能组成回路。
-
(3)复杂度更高。
3.1 算法基本思想 (Basic Idea)
对有 n 个顶点, e 条边,权值为 (eij)n×n 的图:
-
(1)它是通过一个 n 阶方阵求出每两点间的最短路径,即
A={A(0),A(1),⋯,A(n)}
其中:
A(0)A(k)=(aij)n×n(0),=(aij)n×n(k),(aij)(0)(aij)(k)=eij=min{(aij)(k−1),(aik)(k−1)+(akj)(k−1)},k∈[1,n] -
(2) (aij)(0) 是从顶点 vi 到 vj ,以 v0 为中介的最短路径长度;
-
(3) (aij)(k) 是从顶点 vi 到 vj ,以 v0,v2,⋯,vk 为中介的最短路径长度;
-
(4) (aij)(n) 是从顶点 vi 到 vj 的最短路径长度。
-
(5)在计算最短路径长度的同时,更新路径:
Path={P(0),P(1),⋯,P(n)}
以下举例说明,假设原带权有向图:
-
(1)起始矩阵为:
A(0)=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡ABCDEFA......B10.....C.50.20..D30.....E90.1070.99F......⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤
P(0)=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡ABCDEFA−1−1−1−1−1−1B0−1−1−1−1−1C−11−13−1−1D0−1−1−1−1−1E0−123−15F−1−1−1−1−1−1⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤
-
(2)以 v0 即 A 作为中介,A 列全为无穷,则
A(1)=A(0)
-
(3)以 v1 即 B 作为中介:
由于
A(1)[0][1]+A(1)[1][2]<A(1)[0][2](10+50<∞)
则
A(2)[0][2]P(2)[0][2]=A(1)[0][1]+A(1)[1][2]=P(1)[1][2]
得到两个新矩阵:
A(2)=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡ABCDEFA......B10.....C6050.20..D30.....E90.1070.99F......⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤
P(2)=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡ABCDEFA−1−1−1−1−1−1B0−1−1−1−1−1C11−13−1−1D0−1−1−1−1−1E0−123−15F−1−1−1−1−1−1⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤
-
(3)以 v2 即 C 作为中介:
由于
A(2)[0][2]+A(2)[2][4]A(2)[1][2]+A(2)[2][4]A(2)[3][2]+A(2)[2][4]<A(2)[0][4](60+10<90)<A(2)[1][4](50+10<∞)<A(2)[3][4](20+10<70)
则
A(3)[0][4]A(3)[1][4]A(3)[3][4]P(3)[0][4]P(3)[1][4]P(3)[3][4]=A(2)[0][2]+A(2)[2][4]=A(2)[1][2]+A(2)[2][4]=A(2)[3][2]+A(2)[2][4]=P(2)[2][4]=P(2)[2][4]=P(2)[2][4]=70=60=30
得到两个新矩阵:
A(3)=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡ABCDEFA......B10.....C6050.20..D30.....E70601030.99F......⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤
P(3)=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡ABCDEFA−1−1−1−1−1−1B0−1−1−1−1−1C11−13−1−1D0−1−1−1−1−1E2222−15F−1−1−1−1−1−1⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤
-
(4)以此类推 vn−1 则求出矩阵 A(n) 与 P(n) 。
3.2 算法设计思路 (Design Idea)
根据思路,我们需要两个方阵:
-
存储权值的方阵;
-
存储路径的方阵(保存路径的上一个顶点)。
同时,我们还要以每个顶点作为中介求解新矩阵。
对矩阵的循环遍历和每个顶点的遍历得出,对于 n 个顶点的图时间复杂度 O(n3) 。
3.3 C++代码 (C++ Code)
// Author: https://blog.csdn.net/DarkRabbit
// Shortest Path
// 弗洛伊德算法
// params:
// graph: 需要计算的图
// paths: 输出的路径,矩阵记录上一个顶点的下标
// costs: 输出的权值,
// return:
// bool: 是否出错
bool Floyed(AMGraphInt * graph,
std::vector<std::vector<int>>& paths,
std::vector<std::vector<double>>& costs)
{
if (graph == nullptr || !graph->IsOriented()) // 无向图返回
{
return false;
}
int size = graph->GetVertexCount(); // 顶点数量
double infinity = graph->GetDefaultWeight(); // 没有边的权值,即正无穷
paths.assign(size, std::vector<int>(size, -1)); // 初始化空路径方阵(全-1)
costs.assign(size, std::vector<double>(size)); // 定义权值方阵
for (int r = 0; r < size; r++)
{
for (int c = 0; c < size; c++)
{
graph->TryGetWeight(r, c, costs[r][c]); // 初始化权值
// 初始化路径
if (r != c && costs[r][c] < infinity)
{
paths[r][c] = r; // 记录上一个顶点的下标
}
}
}
for (int i = 0; i < size; i++) // 对顶点循环,以此顶点作为中介
{
for (int r = 0; r < size; r++) // 对行循环(源点)
{
if (costs[r][i] == infinity) // 没有路径
{
continue;
}
for (int c = 0; c < size; c++) // 对列循环(终点)
{
if (costs[i][c] == infinity) // 没有路径
{
continue;
}
// 新cost更小,更新cost与路径
if (costs[r][i] + costs[i][c] < costs[r][c])
{
costs[r][c] = costs[r][i] + costs[i][c];
paths[r][c] = paths[i][c];
}
}
}
}
return true;
}
4 主函数与测试 (Main Method and Testing)
4.1 主函数 (Main Method)
#include "abstract_graph.h"
#include "adjacency_matrix.h"
#include "minimum_spanning_tree.h"
#include "shortest_path.h"
#include <vector>
#include <stack>
#include <unordered_map>
#include <iomanip>
#include <iostream>
using namespace std;
typedef Graphs::AMVertexNode<int> AMVertexInt;
typedef Graphs::AdjacencyMatrix<int> AMGraphInt;
void TestAdjacencyMatrix();
void TestKruskal();
void TestPrim();
void TestDujkstra();
void TestFloyed();
int main()
{
//TestAdjacencyMatrix();
//TestKruskal();
//TestPrim();
TestDujkstra();
TestFloyed();
system("pause");
return 0;
}
// 打印邻接矩阵
template<class TGraph>
void PrintMatrix(TGraph& graph);
// 打印 迪杰斯特拉算法 Dujkstra 结果
void PrintDujkstra(AMGraphInt* graph,
const vector<vector<int>>& paths,
const vector<double>& costs)
{
for (int i = 0; i < paths.size(); i++)
{
cout << (char)(i + 'A') << " ";
if (costs[i] == graph->GetDefaultWeight())
{
cout << "无法到达此点" << endl;;
}
else
{
cout << costs[i] << " : ";
for (int j = 0; j < paths[i].size(); j++)
{
cout << (char)(paths[i][j] + 'A') << ' ';
}
cout << endl;
}
}
}
// 迪杰斯特拉算法 Dujkstra
void TestDujkstra()
{
AMGraphInt* graph = new AMGraphInt({ 0, 1, 2, 3, 4, 5 }, true); // 有向图 0-5
graph->InitializeWeights(
{ {0, 1}, {0, 3}, {0, 4}, {1, 2}, {2, 4}, {3, 2}, {3, 4}, {5, 4} },
{ 10, 30, 90, 50, 10, 20, 70, 99 }); // 初始化边
vector<vector<int>> paths;
vector<double> costs;
cout << "----------迪杰斯特拉算法 Dujkstra----------" << endl;
cout << "有向图:" << endl;
cout << "邻接矩阵:" << endl;
PrintMatrix(*graph);
cout << endl;
cout << "从A出发-路径:" << endl;
if (Graphs::ShortestPath::Dujkstra(graph, 0, paths, costs))
{
PrintDujkstra(graph, paths, costs);
}
else
{
cout << "从A出发查找路径失败" << endl;
}
cout << endl;
cout << "从D出发-路径:" << endl;
if (Graphs::ShortestPath::Dujkstra(graph, 3, paths, costs))
{
PrintDujkstra(graph, paths, costs);
}
else
{
cout << "从A出发查找路径失败" << endl;
}
cout << endl;
cout << "--------------------" << endl;
delete graph;
cout << endl;
}
// 打印 弗洛伊德算法 Floyed 结果
void PrintFloyed(AMGraphInt* graph,
int begin,
int end,
const vector<vector<int>>& paths,
const vector<vector<double>>& costs)
{
if (costs[begin][end] == graph->GetDefaultWeight())
{
cout << "无法到达该点。" << endl;
return;
}
double cost = costs[begin][end];
stack<int> path;
path.push(end);
while (paths[begin][end] >= 0)
{
path.push(paths[begin][end]);
end = paths[begin][end];
}
while (!path.empty())
{
cout << (char)(path.top() + 'A');
path.pop();
}
cout << " 消耗 " << cost << endl;
}
// 弗洛伊德算法 Floyed
void TestFloyed()
{
AMGraphInt* graph = new AMGraphInt({ 0, 1, 2, 3, 4, 5 }, true); // 有向图 0-5
graph->InitializeWeights(
{ {0, 1}, {0, 3}, {0, 4}, {1, 2}, {2, 4}, {3, 2}, {3, 4}, {5, 4} },
{ 10, 30, 90, 50, 10, 20, 70, 99 }); // 初始化边
vector<vector<int>> paths;
vector<vector<double>> costs;
cout << "----------弗洛伊德算法 Floyed----------" << endl;
cout << "有向图:" << endl;
cout << "邻接矩阵:" << endl;
PrintMatrix(*graph);
cout << endl;
if (Graphs::ShortestPath::Floyed(graph, paths, costs))
{
cout << "输出的权值矩阵:" << endl;
for (int r = 0; r < costs.size(); r++)
{
for (int c = 0; c < costs.size(); c++)
{
if (costs[r][c] == graph->GetDefaultWeight())
{
cout << " . ";
continue;
}
cout << costs[r][c] << ' ';
}
cout << endl;
}
cout << endl;
cout << "输出的路径矩阵:" << endl;
for (int r = 0; r < paths.size(); r++)
{
for (int c = 0; c < paths.size(); c++)
{
if (paths[r][c] == -1)
{
cout << " . ";
continue;
}
cout << ' ' << (char)(paths[r][c] + 'A') << ' ';
}
cout << endl;
}
cout << endl;
cout << "从A出发到E-路径:" ;
PrintFloyed(graph, 0, 4, paths, costs);
cout << "从D出发到E-路径:" ;
PrintFloyed(graph, 3, 4, paths, costs);
}
else
{
cout << "查找路径失败" << endl;
}
cout << endl;
cout << "--------------------" << endl;
delete graph;
cout << endl;
}
4.2 打印结果 (Print Output)
----------迪杰斯特拉算法 Dujkstra----------
有向图:
邻接矩阵:
A B C D E F
A . 10 . 30 90 .
B . . 50 . . .
C . . . . 10 .
D . . 20 . 70 .
E . . . . . .
F . . . . 99 .
从A出发-路径:
A 0 : A
B 10 : A B
C 50 : A D C
D 30 : A D
E 60 : A D C E
F 无法到达此点
从D出发-路径:
A 无法到达此点
B 无法到达此点
C 20 : D C
D 0 : D
E 30 : D C E
F 无法到达此点
--------------------
----------弗洛伊德算法 Floyed----------
有向图:
邻接矩阵:
A B C D E F
A . 10 . 30 90 .
B . . 50 . . .
C . . . . 10 .
D . . 20 . 70 .
E . . . . . .
F . . . . 99 .
输出的权值矩阵:
. 10 50 30 60 .
. . 50 . 60 .
. . . . 10 .
. . 20 . 30 .
. . . . . .
. . . . 99 .
输出的路径矩阵:
. A D A C .
. . B . C .
. . . . C .
. . D . C .
. . . . . .
. . . . F .
从A出发到E-路径:ADCE 消耗 60
从D出发到E-路径:DCE 消耗 30
--------------------
请按任意键继续. . .