前言
博客记录基础算法设计中求图最短路径的两种经典算法,迪杰斯特拉算法(Dijkstra)和弗洛伊德算法(Floyd)。
Dijkstra算法
算法归类:贪心算法
应用领域:不含负权重的图
解决问题:图的单源最短路径问题
算法效率:O(|V|2) 采用权重矩阵表示图,无序数组表示优先队列;O(|E|log|V|)采用邻接链表表示图,最小堆表示优先队列
算法思想
算法属于贪心算法。
将顶点集合V分为两组,S 已经求出顶点的集合(初始时只含有源点V0)和T (T=V-S)尚未确定的顶点集合。
将T中的顶点按照递增的次序加入到S中,保证:1.从源点V0到S中其他各顶点的长度都不大于从V0到T中任意顶点的最短路径长度;2.每个顶点对应一个距离值。S中的顶点:从V0到此顶点的长度;T中的顶点:从V0到次顶点的只包括S中顶点作为中间顶点的最短路径长度。
依据:可以证明V0到T中顶点Vk的,或是从V0到Vk的直接路径的权值;或是从V0经S中顶点到Vk的路径权值之和。
算法设计(邻接矩阵表示图,无序数组表示优先队列)
#include<iostream>
#define maxv 101
#define maxe 100
#define INF 1000000
using namespace std;
void Dij(int arc[][maxv],int n,int path[maxv]){
//定义源点到其他节点的路径的最小值
int shortest[maxv];
//利用flag标记区分 S和 T集合 S为1 T为0
int finished[maxv];
for(int i=0;i<n;i++){
//定义T集合
finished[i]=0;
//V0节点到其他节点的最短路径 均为V0到Vi的数值
shortest[i]=arc[0][i];
//定义中间顶点路径
path[i] = -;
}
//将V0加入到S中
finished[0]=1;
//临时标记
int k;
for(int i=1;i<n;i++){
int min = INF;
for(int j=0;j<n;j++){
if(!finished[j]&&min>shortest[j]){
min = shortest[j];
k = j;
}
}
//将T中距离V0最近的下一个节点找到 并加入到S中
finished[k] = 1;
for(int j=0;j<n;j++){
if(!finished[j]&&shortest[j]>min+arc[k][j]){
//通过K顶点的方案更短 修正路径方案
shortest[j] = min+arc[k][j];
//记录中间路径
path[j] = k;
}
}
}
//输出格式的控制
for(int i=1;i<n;i++){
int j=path[i];
cout<<"0-"<<i<<" 距离值:"<<shortest[i]<<" 经由:"<<i<<" "<<j;
while(j){
cout<<" "<<path[j];
j= path[j];
}
cout<<endl;
}
}
/*
int main(){
int n,m;
int arc[maxv][maxv];
int path[maxv];
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
if(i==j)
arc[i][j] = 0;
else
arc[i][j] = INF;
}
int u,v,w;
for(int i=0;i<m;i++){
cin>>u>>v>>w;
arc[u][v] = w;
}
Dij(arc,n,path);
return 0;
}
带权有向图
6 9
0 2 5
0 3 30
1 0 2
1 4 8
2 5 7
2 1 15
4 3 4
5 3 10
5 4 18
输出结果
从0到1距离是:20 0->2->1
从0到2距离是: 5 0->2
从0到3距离是:22 0->2->5->3
从0到4距离是:28 0->2->1->4
从0到5距离是:12 0->2->5
*/
/*
int main(){
//输入为 图的邻接矩阵存储方式
int n,arc[maxv][maxv];
//定义一个存储路径的path矩阵
int path[maxv];
//这里 n是该图的顶点个数
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>arc[i][j];
Dij(arc,n,path);
return 0;
}
输入带权无向图
5
0 3 1000000 7 1000000
3 0 4 2 1000000
1000000 4 0 5 6
7 2 5 0 4
1000000 1000000 6 4 0
输出结果:0作为起始点
0-1 距离值:3 经由:1 0
0-2 距离值:7 经由:2 1 0
0-3 距离值:5 经由:3 1 0
0-4 距离值:9 经由:4 3 1 0
*/
运行结果下图:
上述算法中算法的效率为O(n^2) ,这里可以利用最小堆的方法来对优先队列进行优化,使效率达到O(mlogn),这里m指的是图的边,n指的是图的顶点。稀疏图的情况下效率更高。(代码参考刘汝佳《算法竞赛入门经典第2版》)将整个的dijkstra算法封装成一个结构体,可以直接调用,重用效率较高!
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<sstream>
using namespace std;
typedef long long ll;
const int maxn = 1e3 + 10;
const int INF = 1 << 30;
int T, n, m;
struct Edge{
int from,to,dist;
Edge(int u,int v, int d):from(u),to(v),dist(d) {}
};
struct HeapNode{
int d,u;//d为距离 u为起点
bool operator<(const HeapNode& rhs) const{
return d>rhs.d;//这样优先队列先取出d小的
}
};
struct Dijkstra{
int n,m;//n个顶点 m条边
vector<Edge> edges;//存边的信息
vector<int> G[maxn];//G[i]表示起点为i的边的序号集
bool done[maxn];//是否已经永久标号
int d[maxn];//S点到个顶点的距离
int p[maxn];//最短路中的上一条弧 逆向记录路径
void init(int n){
this->n = n;
for(int i=0; i<n; i++) G[i].clear();
edges.clear();
}
void AddEdge(int from,int to,int dist){
edges.push_back(Edge(from,to,dist));
m = edges.size();
G[from].push_back(m-1);//存以from为起点的下一条边
}
//以s为起点 获取最短路径
void dijkstra(int s){
priority_queue<HeapNode> Q;
for(int i=0;i<n;i++) d[i] = INF;
d[s] = 0;
memset(done,0,sizeof(done));
memset(p,-1,sizeof(p));
Q.push((HeapNode){0,s});
while(!Q.empty()){
HeapNode now = Q.top();
Q.pop();
int u= now.u;//当前起点
if(done[u]) continue;//如果已经加入集合,continue
done[u] = true;
for(int i=0;i<G[u].size();i++){
Edge& e = edges[G[u][i]];//引用节省代码
if(d[e.to]>d[u]+e.dist){
d[e.to] = d[u]+e.dist;
p[e.to] = G[u][i];//记录e.to前的边的编号,p存的是边的下标,这样可以通过边找出之前的点以及每条路的路径,如果用邻接矩阵存储的话这里可以直接存节点u
Q.push((HeapNode){d[e.to],e.to});
}
}
}
}
//输出控制
void output(int u)
{
for(int i = 0; i < n; i++)
{
if(i == u)continue;
printf("从%d到%d距离是:%2d ", u, i, d[i]);
stack<int>q;//存的是边的编号
int x = i;//x就是路径上所有的点
while(p[x] != -1)
{
q.push(x);
x = edges[p[x]].from;//x变成这条边的起点
}
cout<<u;
while(!q.empty())
{
cout<<"->"<<q.top();
q.pop();
}
cout<<endl;
}
}
};
//定义一个结构体变量
Dijkstra demo;
int main(){
//输入该图的顶点数目和边的数目
cin>>n>>m;
demo.init(n);
for(int i=0;i<m;i++){
// 一条边的起点 终点 权值
int u,v,w;
cin>>u>>v>>w;
demo.AddEdge(u,v,w);
}
int u = 0;//定义源点为 0节点
demo.dijkstra(u);
demo.output(u);
return 0;
}
/*
带权有向图
6 9
0 2 5
0 3 30
1 0 2
1 4 8
2 5 7
2 1 15
4 3 4
5 3 10
5 4 18
输出结果
从0到1距离是:20 0->2->1
从0到2距离是: 5 0->2
从0到3距离是:22 0->2->5->3
从0到4距离是:28 0->2->1->4
从0到5距离是:12 0->2->5
*/
运行结果
Floyd算法
算法归类:动态规划
应用领域:不含负权重的图
解决问题:图的单源最短路径问题
算法效率:O(n^3)
算法思想
算法属于动态规划类算法。
通过一个图的权值矩阵求出它的每两点间的最短路径矩阵。
从图的带权邻接矩阵A=[a(i,j)] n×n开始,递归地进行n次更新,即由矩阵D(0)=A,按一个公式,构造出矩阵D(1);又用同样地公式由D(1)构造出D(2);……;最后又用同样的公式由D(n-1)构造出矩阵D(n)。矩阵D(n)的i行j列元素便是i号顶点到j号顶点的最短路径长度,称D(n)为图的距离矩阵,同时还可引入一个后继节点矩阵path来记录两点间的最短路径。
采用松弛技术(松弛操作),对在i和j之间的所有其他点进行一次松弛。所以时间复杂度为O(n^3)。
状态转移方程: map[i,j]:=min{map[i,k]+map[k,j],map[i,j]};
map[i,j]表示i到j的最短距离,K是穷举i,j的断点,map[n,n]初值应该为0,或者按照题目意思来做。
当然,如果这条路没有通的话,还必须特殊处理,比如没有map[i,k]这条路。
算法设计
#include<iostream>
#define INF 1000000
#define maxv 101
#define maxe 100
using namespace std;
void Floyd(int cost[][101],int n)
{
int A[maxv][maxv];
int path[maxv][maxv];
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
A[i][j]=cost[i][j];
if(A[i][j]==INF)
path[i][j]=-1;
else
path[i][j]=i;
}
}
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
if(A[i][j]>A[i][k]+A[k][j])
{
A[i][j]=A[i][k]+A[k][j];
path[i][j]=k;
}
}
//输出结果
cout<<"矩阵每行的结果:"<<endl;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
cout<<"Start node:"<<i<<" "<<"End node:"<<j<<" "<<"Dis:"<<A[i][j]<<endl;
cout<<endl;
}
cout<<"整个矩阵的计算结果:"<<endl;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++)
cout<<A[i][j]<<" ";
cout<<endl;
}
}
int main()
{
int cost[maxv][maxv];
int n;
while(cin>>n)
{
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>cost[i][j];
//调用Floyd算法
Floyd(cost,n);
}
return 0;
}
/*
6
0 35 18 7 23 42
8 0 22 34 1000000 21
24 19 0 40 27 11
1000000 8 32 0 16 38
19 25 10 28 0 16
36 17 1000000 12 5 0
*/
运行结果