一、最短路径问题介绍
1、问题解释: 从图中的某个顶点出发到达另外一个顶点的所经过的边的权重和最小的一条路径,称为最短路径
2、解决问题的算法:
- 迪杰斯特拉算法(Dijkstra算法)
- 弗洛伊德算法(Floyd算法)
- SPFA算法
- bellman-ford算法
二、Dijkstra算法
1、算法特点:Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等。注意该算法要求图中不存在负权边。
2、问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径。(单源最短路径)
3、算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
4、算法步骤:
a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
d.重复步骤b和c直到所有顶点都包含在S中。
5、执行动画过程如下图:
5、算法代码实现:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 0x3f3f3f3f;
const int p = 1005;
int a[p][p];//存放点与点的距离
int dist[p];//源点到各个点的最短距离
int pre[p];//计算过的点
int s[p];//判断是否已经加入pre,是否已访问
int n,m;
void Dijkstra(int v)
{
int i,j;
for(i = 0;i < n;++i)
{
if(i != v)
{
dist[i] = a[v][i];
}
s[i] = false;
}
s[v] = true;
dist[v] = 0;
pre[0]=v;
//对各个数组进行初始化
for(i = 0;i < n;++i)
{
int minset = MAXN;
int u = v;
for(j = 0; j < n;++j)
{
if(!s[j] && dist[j] < minset)
//找到剩余节点中最小的节点
{
u = j;
minset = dist[u];
}
}
s[u] = true;
//将节点标记为以访问,相当于加入s数组中
for(j = 0;j < n;++j)
{
if(!s[j] && a[u][j] < MAXN)
//a[u][j] < MAXN指更新u的可达点
{
if(dist[u] + a[u][j] < dist[j])
{
dist[j] = dist[u] + a[u][j];
pre[j] = u;
//储存得出最短路径的前一个节点,用于路径还原
}
}
}
}
}
int main()
{
int t;
cin>>t;
while(~scanf("%d %d",&n,&m))
{
memset(a,MAXN,sizeof(a));
memset(dist,MAXN,sizeof(dist));
int i,j;
for(i = 0;i < m;++i)
{
int x,y,d;
scanf("%d %d %d",&x,&y,&d);
a[x-1][y-1]=d;
a[y-1][x-1]=d;
}
//int v;
//scanf("%d",&v);
Dijkstra(0);
for(int i = 0;i < n;++i)
{
printf("%d ",dist[i]);
}
printf("\n");
/*for(int i = 0;i < n;++i)
{
printf("%d ",s[i]);
}
printf("\n");
for(int i = 0;i < n;++i)
{
printf("%d ",pre[i]);
}
printf("\n");*/
}
return 0;
}
6、算法实例
先给出一个无向图:
用Dijkstra算法找出以A为起点的单源最短路径步骤如下:
三、Floyd算法
1、定义:Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。Floyd-Warshall算法的时间复杂度为O(N3),空间复杂度为O(N2)。
2、算法思想原理:Floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径。从动态规划的角度看问题,我们需要为这个目标重新做一个诠释(这个诠释正是动态规划最富创造力的精华所在)。
从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,我们假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
3、算法描述:
a.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
b.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
3).Floyd算法过程矩阵的计算----十字交叉法
方法:两条线,从左上角开始计算一直到右下角 如下所示
给出矩阵,其中矩阵A是邻接矩阵,而矩阵Path记录u,v两点之间最短路径所必须经过的点。
相应计算方法如下:
最后A3即为所求结果
4、算法代码实现:
(1)用Floyd求无向图的最小环
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 100005;
const int p = 105;
int dist[p][p];
int mp[p][p];
int main()
{
int n,m;
while(~scanf("%d %d",&n,&m))
{
int i,j,k;
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
dist[i][j] = MAXN;
mp[i][j] = MAXN;
}
}
for(i = 0;i < m;++i)
{
int a,b,c;
scanf("%d %d %d",&a,&b,&c);
if(c < mp[a-1][b-1])
dist[a-1][b-1]=dist[b-1][a-1]=mp[b-1][a-1]=mp[a-1][b-1]=c;
}
int ans = MAXN;
for(k = 0;k < n;++k)
{
for(i = 0;i < n;++i)
{
for(j = i+1;j < n;++j)
//这里用i+1,防止i,j是不同的两个点
{
if(ans > dist[i][j] + mp[i][k] + mp[k][j])
{
ans = dist[i][j] + mp[i][k] + mp[k][j];
}
}
}
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
if(dist[i][j] > dist[i][k] + dist[k][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
if(ans == MAXN)
printf("It's impossible.\n");
else
printf("%d\n",ans);
}
return 0;
}
(2)Floyd的模板:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 0x3f3f3f3f;
const int p = 1005;
int dist[p][p];
int path[p][p];
int mp[p][p];
int n,m;
int Floyd()
{
int i,j,k;
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
dist[i][i]=0;
mp[i][i]=0;
}
}
int ans = MAXN;
for(k = 0;k < n;++k){
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
if(dist[i][k] + dist[k][j] < dist[i][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
path[i][j] = k;
}
}
}
}
return ans;
}
int main()
{
int t;
cin>>t;
while(t--)
{
int i,j;
memset(path,-1,sizeof(path));
memset(dist,MAXN,sizeof(dist));
memset(mp,MAXN,sizeof(mp));
scanf("%d %d",&n,&m);
for(int i = 0;i < m;++i)
{
int x,y,d;
scanf("%d %d %d",&x,&y,&d);
dist[x][y]=d;
dist[y][x]=d;
mp[x][y]=d;
mp[y][x]=d;
}
printf("%d\n",Floyd());
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
printf("%d ",dist[i][j]);
}
cout<<endl;
}
for(i = 0;i < n;++i)
{
for(j = 0;j < n;++j)
{
printf("%d ",path[i][j]);
}
cout<<endl;
}
}
return 0;
}
四、SPFA算法
1、SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,它是一种十分高效的最短路算法。很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。
但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。
2、实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。
3、算法图解:
给定一个有向图,求A~E的最短路。
源点A首先入队,并且AB松弛
扩展与A相连的边,B,C 入队并松弛。
B,C分别开始扩展,D入队并松弛
D出队,E入队并松弛。
E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8
以上就是SPFA算法的过程。
4、算法模板:
#include "bits/stdc++.h"
using namespace std;
const int maxN = 200010 ;
struct Edge
{
int to , next , w ;
} e[ maxN ];
int n,m,cnt,p[ maxN ],Dis[ maxN ];
int In[maxN ];
bool visited[ maxN ];
void Add_Edge ( const int x , const int y , const int z )
{
e[ ++cnt ] . to = y ;
e[ cnt ] . next = p[ x ];
e[ cnt ] . w = z ;
p[ x ] = cnt ;
return ;
}
bool Spfa(const int S)
{
int i,t,temp;
queue<int> Q;
memset ( visited , 0 , sizeof ( visited ) ) ;
memset ( Dis , 0x3f , sizeof ( Dis ) ) ;
memset ( In , 0 , sizeof ( In ) ) ;
Q.push ( S ) ;
visited [ S ] = true ;
Dis [ S ] = 0 ;
while( !Q.empty ( ) )
{
t = Q.front ( ) ;Q.pop ( ) ;visited [ t ] = false ;
for( i=p[t] ; i ; i = e[ i ].next )
{
temp = e[ i ].to ;
if( Dis[ temp ] > Dis[ t ] + e[ i ].w )
{
Dis[ temp ] =Dis[ t ] + e[ i ].w ;
if( !visited[ temp ] )
{
Q.push(temp);
visited[temp]=true;
if(++In[temp]>n)return false;
}
}
}
}
return true;
}
int main ( )
{
int S , T ;
scanf ( "%d%d%d%d" , &n , &m , &S , &T ) ;
for(int i=1 ; i<=m ; ++i )
{
int x , y , _ ;
scanf ( "%d%d%d" , &x , &y , &_ ) ;
Add_Edge ( x , y , _ ) ;
}
if ( !Spfa ( S ) ) printf ( "FAIL!\n" ) ;
else printf ( "%d\n" , Dis[ T ] ) ;
return 0;
}
五、bellman-ford算法
1、单源最短路径。给定一个图,和一个源顶点src,找到从src到其它所有所有顶点的最短路径,图中可能含有负权值的边。Dijksra的算法是一个贪婪算法,时间复杂度是O(VLogV)(使用最小堆)。但是迪杰斯特拉算法在有负权值边的图中不适用,Bellman-Ford适合这样的图。在网络路由中,该算法会被用作距离向量路由算法。Bellman-Ford也比迪杰斯特拉算法更简单和同时也适用于分布式系统。但Bellman-Ford的时间复杂度是O(VE),这要比迪杰斯特拉算法慢。(V为顶点的个数,E为边的个数)
2、算法描述
输入:图 和 源顶点src
输出:从src到所有顶点的最短距离。如果有负权回路(不是负权值的边),则不计算该最短距离,没有意义,因为可以穿越负权回路任意次,则最终为负无穷。
3、算法步骤
1.初始化:将除源点外的所有顶点的最短距离估计值 dist[v] ← +∞, dist[s] ←0;
2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 dist[v]中。
关于该算法的证明也比较简单,采用反证法,具体参考:http://courses.csail.mit.edu/6.006/spring11/lectures/lec15.pdf
该算法是利用动态规划的思想。该算法以自底向上的方式计算最短路径。
它首先计算最多一条边时的最短路径(对于所有顶点)。然后,计算最多两条边时的最短路径。外层循环需要执行|V|-1次。
4、例子
一下面的有向图为例:给定源顶点是0,初始化源顶点距离所有的顶点都是是无穷大的,除了源顶点本身。因为有5个顶点,因此所有的边需要处理4次。
按照以下的顺序处理所有的边:(B,E), (D,B), (B,D), (A,B), (A,C), (D,C), (B,C), (E,D).
第一次迭代得到如下的结果(第一行为初始化情况,最后一行为最终结果):
当 (B,E), (D,B), (B,D) 和 (A,B) 处理完后,得到的是第二行的结果。
当 (A,C) 处理完后,得到的是第三行的结果。
当 (D,C), (B,C) 和 (E,D) 处理完后,得到第四行的结果。
第一次迭代保证给所有最短路径最多只有1条边。当所有的边被第二次处理后,得到如下的结果(最后一行为最终结果):
第二次迭代保证给所有最短路径最多只有2条边。我们还需要2次迭代(即所谓的松弛操作),就可以得到最终结果。
5、代码
/*-------------------------------------
* Bellman-Ford算法(单源最短路径)
*
------------------------------------*/
#include <iostream>
using namespace std;
//表示一条边
struct Edge{
// 起点
int src;
// 终点
int dest;
// 权重
int weight;
};
//带权值的有向图
struct Graph{
// 顶点的数量
int V;
// 边的数量
int E;
// 用边的集合 表示一个图
Edge* edge;
};
// 创建图
Graph* CreateGraph(int v,int e){
Graph* graph = (Graph*)malloc(sizeof(Graph));
graph->E = e;
graph->V = v;
graph->edge = (Edge*)malloc(e*sizeof(Edge));
return graph;
}
// 打印结果
void Print(int dist[],int n){
cout<<"单源最短路径:"<<endl;
for(int i = 0;i < n;++i){
if(dist[i] == INT_MAX){
cout<<"与节点"<<i<<"距离->无穷大"<<endl;
}//if
else{
cout<<"与节点"<<i<<"距离->"<<dist[i]<<endl;
}
}//for
}
// 单源最短路径
bool BellmanFord(Graph* graph,int src){
int v = graph->V;
int e = graph->E;
// 存储距离
int dist[v];
// 初始化
for(int i = 0;i < v;++i){
dist[i] = INT_MAX;
}//for
dist[src] = 0;
// v-1次操作
Edge edge;
int a,b,weight;
for(int i = 1;i < v;++i){
// 对e条边进行松弛
for(int j = 0;j < e;++j){
edge = graph->edge[j];
a = edge.src;
b = edge.dest;
weight = edge.weight;
if(dist[a] != INT_MAX && dist[a]+weight < dist[b]){
dist[b] = dist[a]+weight;
}//if
}//for
}//for
// 检测负权回路
bool isBack = false;
for(int i = 0;i < e;++i){
edge = graph->edge[i];
a = edge.src;
b = edge.dest;
weight = edge.weight;
if(dist[a] != INT_MAX && dist[a]+weight < dist[b]){
isBack = true;
break;
}//if
}//for
// 打印结果
Print(dist,v);
return isBack;
}
int main(){
int v = 7;
int e = 9;
Graph* graph = CreateGraph(v,e);
graph->edge[0].src = 0;
graph->edge[0].dest = 1;
graph->edge[0].weight = -1;
graph->edge[1].src = 0;
graph->edge[1].dest = 2;
graph->edge[1].weight = 4;
graph->edge[2].src = 1;
graph->edge[2].dest = 2;
graph->edge[2].weight = 3;
graph->edge[3].src = 1;
graph->edge[3].dest = 3;
graph->edge[3].weight = 2;
graph->edge[4].src = 1;
graph->edge[4].dest = 4;
graph->edge[4].weight = 2;
graph->edge[5].src = 3;
graph->edge[5].dest = 2;
graph->edge[5].weight = 5;
graph->edge[6].src = 3;
graph->edge[6].dest = 1;
graph->edge[6].weight = 1;
graph->edge[7].src = 4;
graph->edge[7].dest = 3;
graph->edge[7].weight = -3;
graph->edge[8].src = 5;
graph->edge[8].dest = 6;
graph->edge[8].weight = 2;
bool result = BellmanFord(graph,0);
if(result){
cout<<"图中存在回路"<<endl;
}//if
else{
cout<<"图中不存在回路"<<endl;
}//else
return 0;
}
6、适用范围:
- 单源最短路径(从源点到其他所有点v);
- 有向图&无向图;
- 边权可正可负
- 差分约束系统
六、四大算法对比
bellman-ford可以用于边权为负的图中,图里有负环也可以,如果有负环,算法会检测出负环。
时间复杂度O(VE);
dijkstra只能用于边权都为正的图中。
时间复杂度O(n2);
spfa是个bellman-ford的优化算法,本质是bellman-ford,所以适用性和bellman-ford一样。(用队列和邻接表优化)。
时间复杂度O(KE);
floyd可以用于有负权的图中,即使有负环,算法也可以检测出来,可以求任意点的最短路径,有向图和无向图的最小环和最大环。
时间复杂度O(n3);
任何题目中都要注意的有四点事项:图是有向图还是无向图、是否有负权边,是否有重边,顶点到自身的可达性。
参考:https://blog.csdn.net/qq_35644234/article/details/60870719
https://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html