Dijkstra算法
1.单源路径
2.有向图或无向图
3.无负边权
1. Dijkstra算法思想
设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组。
第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将 加入到集合S中,直到全部顶点都加入到S中,算法就结束了)。
第二组为其余未确定最短路径的顶点集合(用U表示)。
按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度。U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
2.
(1)初始时,S只包含源点,即S,v的距离为0。U包含除v外的其他顶点,U中顶点u距离为边上的权(若v与u有 边)。
(2)从U中选取一个距离v最小的顶点k,把k加入S中(该选定的距离就是v到k的最短路径长度)。
(3)以k为新考虑的中间点,修改U中各顶点的距离。若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值为顶点k的距离加上边上的权值。
(4)重复步骤(2)和(3)直到所有顶点都包含在S中。
3. Dijkstra算法举例说明
如下图,设A为源点,求A到其他各顶点(B、C、D、E、F)的最短路径。线上所标注为相邻线段之间的距离,即权值。
图一:Dijkstra无向图
算法执行步骤如下表:
4.Dijkstra算法实现
#include <iostream>
#define MAX 100
#define INF 1000000000
using namespace std;
int map[MAX][MAX];
int Dijkstra (int map[][MAX],int n, int s,int e) //n个节点,s为起点,e为终点
{
int dis[MAX];
int mark[MAX]; //记录被选中的结点
int i,j,k = 0;
for(i = 1 ; i <= n ; i++) //初始化所有结点,每个结点都没有被选中
mark[i] = 0;
for(i = 1 ; i <= n ; i++) //将每个结点到start结点weight记录为当前distance
dis[i] = map[s][i];
mark[s] = 1; //start结点被选中
dis[s] = 0; //将start结点的的距离设置为0
int min ; //设置最短的距离。
for(i = 1 ; i < n; i++)
{
min = INF;
for(j = 1; j <=n;j++)
{
if(mark[j] == 0 && dis[j] < min) //未被选中的结点中,距离最短的被选中
{
min = dis[j] ;
k = j;
}
}
mark[k] = 1; //标记为被选中
for(j = 1 ; j <= n ; j++)
{
if( mark[j] == 0 && dis[j] > (dis[k] + map[k][j])) //修改剩余结点的最短距离
dis[j] = dis[k] + map[k][j];
}
}
return dis[e];
}
int main()
{
int n,m,a,b,dis;
int i,j;
while(scanf("%d %d",&n,&m))
{
if(n == 0 || m == 0) break;
for(i=1;i<=n;i++)
for(j =1;j<= n;j++)
map[i][j] = INF;
for(i = 0 ; i < m ;i++)
{
scanf("%d %d %d",&a,&b,&dis);
if(dis < map[a][b] )
map[a][b] = map[b][a] = dis;
}
int ans = Dijkstra(map,n,1,n);
printf("%d\n",ans);
}
return 0;
}
Bellman-ford算法
1.单源路径
2.有向图或无向图
3.边权可正可负
1.Bellman-Ford算法思想
对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。
对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。
2.Bellman-Ford算法流程:
(1)初始化:将除源点外的所有顶点的最短距离估计值 d[v] ← +∞, d[s] ←0。
(2)迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最 短距离。(运行v-1次)
(3)检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明 问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。
描述性证明:
1).图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。
2).从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
3).在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。
4).每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)
5).如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。
3.Bellman-Ford算法实现
#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 0x3f3f3f3f
#define N 1010
int n, m, l; //点,边,起点
int dis[N], pre[N];
struct Edge //边
{
int u, v;
int cost;
}edge[N];
int Bellman_Ford()
{
int i,j;
for( i = 1; i <= n; i++) //初始化
dis[i] = (i == l ? 0 : MAX);
for( i = 1; i <= n - 1; i++)
for( j = 1; j <= m; j++)
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反)
{
dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
pre[edge[j].v] = edge[j].u;
}
int flag = 1; //判断是否含有负权回路
for( i = 1; i <= m; i++)
if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
{
flag = 0;
break;
}
return flag;
}
void print_path(int root) //打印最短路的路径(反向)
{
while(root != pre[root]) //前驱
{
printf("%d-->", root);
root = pre[root];
}
if(root == pre[root])
printf("%d\n", root);
}
int main()
{
scanf("%d%d%d", &n, &m, &l);
pre[l] = l;
for(int i = 1; i <= m; ++i)
scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
if(Bellman_Ford())
for(int i = 1; i <= n; ++i) //每个点最短路
{
printf("%d\n", dis[i]);
printf("Path:");
print_path(i);
}
else
printf("have negative circle\n");
return 0;
}
Floyd-Warshall算法
Floyd-Warshall算法是解决任意两点间的最短路径的算法。
可以处理有向图或负权值的最短路径问题,同时也被用于计算有向图的传递闭包。
设D_{i,j,k}为从i到j的只以(1..k)集合中的节点为中间节点的最短路径的长度。
若最短路径经过点k,则D_{i,j,k}=D_{i,k,k-1}+D_{k,j,k-1};
若最短路径不经过点k,则D_{i,j,k}=D_{i,j,k-1}。
因此,D_{i,j,k}={min}(D_{i,k,k-1}+D_{k,j,k-1},D_{i,j,k-1})。
设图G中n 个顶点的编号为1到n。
令c [i, j, k]表示从i 到j 的最短路径的长度,其中k 表示该路径中的最大顶点,也就是说c[i,j,k]这条最短路径所通过的中间顶点最大不超过k。
如果G中包含边<i, j>,则c[i, j, 0] =边<i, j> 的长度;若i= j ,则c[i,j,0]=0。
如果G中不包含边<i, j>,则c (i, j, 0)= +∞。c[i, j, n] 则是从i 到j 的最短路径的长度。
对于任意的k>0,通过分析可以得到:中间顶点不超过k 的i 到j 的最短路径有两种可能:该路径含或不含中间顶点k。
若不含,则该路径长度应为c[i, j, k-1],
否则长度为 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取两者中的最小值。
状态转移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。
核心代码:
for(int k =1 ; k <= n ; k ++ ){
for(int i =1 ; i<= n ; i++){
for(int j =1 ;j<=n;j++){
dist[ i ][ j ]= min( dist[ i ][ j ],dist[ i ][ k ]+dist[ k ][ j ] );
}
}
}
Floyd算法实现
#include<iostream>
#include<cstdio>
using namespace std;
int map[11111][11111];
#define INF 999999
int main()
{
int i,j,k;
int n,m;
int a,b,c;
while(cin>>n>>m)
{
if(n==0 && m==0) break;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{
if(i==j)
map[i][j]=0;
else
map[i][j]=INF;
}
for(i=1;i<=m;i++)
{
cin>>a>>b>>c;
map[a][b]=map[b][a]=c;
}
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
for(k=1;k<=n;k++)
{
if(map[j][k]>map[j][i]+map[i][k])
map[j][k]=map[j][i]+map[i][k];
}
cout<<map[1][n]<<endl;
}
return 0;
}
SPFA算法
代码实现模板
邻接表:
long long SPFA(int st)
{
for(int i=1;i<=n;i++)
sp[i]=inf;
sp[1]=0;
queue<int> q;
q.push(st);
while(!q.empty())
{
int kai=q.front();q.pop();
for(int i=H[kai];i!=-1;i=E[i].next)
{
if(sp[E[i].v]>E[i].count+sp[kai]){
sp[E[i].v]=E[i].count+sp[kai];
q.push(E[i].v);
}
}
}
long long ans=0;
for(int i=1;i<=n;i++)
ans+=sp[i];
return ans;
}
邻接矩阵:
void spfa(int s,int dis[])
{
int i,pre[N];
bool used[N];
queue<int> q;
memset(used,0,sizeof(used));
memset(pre,-1,sizeof(pre));
for(i=0; i<N; i++)
dis[i]=inf;
dis[s]=0;
used[s]=true;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
used[u]=false;
for(i=0; i<map[u].size(); i++)
{
Node p=map[u][i];
if(dis[p.v]>dis[u]+p.len)
{
dis[p.v]=dis[u]+p.len;
pre[p.v]=u;
if(!used[p.v])
{
used[p.v]=true;
q.push(p.v);
}
}
}
}
}