Dijkstra算法:
解决的问题:
带权重的有向图上单源最短路径问题。且权重都为非负值。如果采用的实现方法合适,Dijkstra运行时间要低于Bellman-Ford算法。
思路:
如果存在一条从i到j的最短路径(Vi…Vk,Vj),Vk是Vj前面的一顶点。那么(Vi…Vk)也必定是从i到k的最短路径。为了求出最短路径,Dijkstra就提出了以最短路径长度递增,逐次生成最短路径的算法。譬如对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist[i]+matrix[i][j]},应用了贪心的思想。根据这种思路,直接给出Dijkstra算法的伪代码,他可用于计算正权图的单源最短路径,同时适用于无向图和有向图。
清除所有点的标号
设d[0]=0,其他d[i]=INF
循环n次
{
在所有点的标号中,选出d值最小的结点x
给结点x标记
对于从x出发的所有边(x,y),更新d[y]=min{d[y],d[x]+w(x,y)}
}
除了求出最短路的长度外,使用Dijkstra算法也能很方便地打印出结点0到所有节点的最短路本身.
代码实现:
void dijkstra(int start)//从start点开始
{
int i,j,k;
memset(vis,0,sizeof(vis));//标记是否访问过
for(i=1; i<=n; i++)//n为总点数
{
if(i==start)
dis[i]=0;
else
dis[i]=INF;
}
for(i=1; i<=n; i++)
{
int r;
int min=INF;
for(j=1; j<=n; j++)
if(!vis[j]&&dis[j]<min)
{
min=dis[j];
r=j;
}
vis[r]=1;
for(k=1; k<=n; k++)//对所有从r出发的边进行松弛
if(dis[k]<(dis[r]+g[r][k]))
dis[k]=dis[k];
else
dis[k]=dis[r]+g[r][k];
}
return;
}
Floyd算法:
负权重的边可以存在,但不能存在权重为负值的环路
floyd算法是一个很强大的算法,它可以计算任意两点之间的最短路径,其边可以为负值。
对于floyd算法是我刚刚开始接触最短路算法中最喜欢的了,因为它的代码简短,便于理解,而且功能也很强大,虽然有点短腿但我还是很喜欢这个代码。
floyd算法是三重for 的嵌套。对于这个算法给出《挑战程序设计》中的证明 :
证明:
对于0~k,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不仅过顶点k的情况下,d[k][i][j] = d[k-1][i][j]。通过顶点k的情况,d[k][i][j]
= d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j] = min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以用同一个数组不断进行如下的操作:
d[i][j] = min(d[i][j],d[i][k]+d[k][j])的更新来实现。
floyd算法的时间复杂度为O(|V|³)。 450450450<10的8次方
下面给出floyd算法的程序。
int n,s,t;
void floyd()
{
int i, j, k;
for(k=0;k<n;k++)
for(i=0;i<n;i++)
for(j=0;j<n;j++)
G[i][j] = min(G[i][j],G[i][k]+G[k][j]);
printf("%d\n",G[s][t]==MAX?-1:G[s][t]);
}
int main()
{
int i, j, m, u, v, w;
while(scanf("%d %d",&n,&m)==2){
for(i=0;i<n;i++)
for(j=0;j<n;j++)
G[i][j] = i==j?0:MAX;
while(m--){
scanf("%d %d %d",&u,&v,&w);
if(G[u][v]>w)
G[u][v] = G[v][u] = w;
}
scanf("%d %d",&s,&t);
floyd();
}
return 0;
}
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<climits>
const int N = 101;
int map[N][N];
void Floyd(int n) {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if((map[i][k] != INT_MAX) && (map[k][j] != INT_MAX)
&&(map[i][j] > map[i][k] + map[k][j] || map[i][j] == INT_MAX)) {
map[i][j] = map[i][k] + map[k][j];
}
}
}
}
}
int main() {
int n, m;
while (~scanf("%d%d", &n, &m), n | m) {
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
map[i][j] = map[j][i] = INT_MAX;
}
}
while (m--) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
map[u][v] = map[v][u] = w;
}
Floyd(n);
printf("%d\n",map[1][n]);
}
Bellman-Ford算法
解决的问题:
一般情况下的单源最短路径问题,这里权重可以为负值。
Bellman-ford算法返回一个布尔值,一表明是否存在一个从源结点可以到达的权重为负的环路。如果存在这样一个环路,算法将告诉我们不存在解决方案,如果没有这种环路的存在算法将给出最短路径和他们的权重。
Bellman-Ford算法的流程如下:
给定图G(V, E)(其中V、E分别为图G的顶点集与边集),源点s,数组Distant[i]记录从源点s到顶点i的路径长度,初始化数组Distant[n]为, Distant[s]为0;
以下操作循环执行至多n-1次,n为顶点数:
对于每一条边e(u, v),如果Distant[u] + w(u, v) < Distant[v],则另Distant[v] = Distant[u]+w(u, v)。w(u, v)为边e(u,v)的权值;
若上述操作没有对Distant进行更新,说明最短路径已经查找完毕,或者部分点不可达,跳出循环。否则执行下次循环;
为了检测图中是否存在负环路,即权值之和小于0的环路。对于每一条边e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的边,则图中存在负环路,即是说改图无法求出 单源最短路径。否则数组Distant[n]中记录的就是源点s到各顶点的最短路径长度。
可知,Bellman-Ford算法寻找单源最短路径的时间复杂度为O(V*E).
Bellman-Ford算法可以大致分为三个部分
第一,初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算。
第三,遍历途中所有的边(edge(u,v)),判断是否存在这样情况:
d(v) > d (u) + w(u,v)
则返回false,表示途中存在从源点可达的权为负的回路。
之所以需要第三部分的原因,是因为,如果存在从源点可达的权为负的回路。则 应为无法收敛而导致不能求出最短路径。
#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 0x3f3f3f3f
#define N 1010
int nodenum, edgenum, original; //点,边,起点
typedef struct Edge //边
{
int u,v;
int cost;
} Edge;
Edge edge[N];
int dis[N], pre[N];
bool Bellman_Ford()
{
for(int i = 1; i <= nodenum; ++i)
{
if(i==original)
dis[i]=0;
else
dis[i]=MAX;
}
for(int i = 1; i <= nodenum - 1; ++i)//循环n-1次
for(int j = 1; j <= edgenum; ++j)//遍历每条边
{
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)
{
dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
printf("%d ",dis[edge[j].v]);
pre[edge[j].v] = edge[j].u;
}
printf("%d ",dis[edge[j].v]);
}
bool flag = 1; //判断是否含有负权回路
for(int i = 1; i <= edgenum; ++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", &nodenum, &edgenum, &original);
pre[original] = original;
for(int i = 1; i <= edgenum; ++i)
{
scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
}
if(Bellman_Ford())
for(int i = 1; i <= nodenum; ++i) //每个点最短路
{
printf("%d\n", dis[i]);
printf("Path:");
print_path(i);
}
else
printf("have negative circle\n");
return 0;
}
spfa算法:
算法流程
算法大致流程是用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束。
这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法
SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的时间复杂度内求出源点到其他所有点的最短路径,可以处理负边。SPFA的实现甚至比Dijkstra或者Bellman_Ford还要简单:
设Dist代表S到I点的当前最短距离,Fa代表S到I的当前最短路径中I点之前的一个点的编号。开始时Dist全部为+∞,只有Dist[S]=0,Fa全部为0。
维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点S。用一个布尔数组记录每个点是否处在队列中。
每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断Dist[v]+len是否小于 Dist[u],若小于则改进Dist[u],将Fa[u]记为v,并且由于S到u的最短距离变小了,有可能u可以改进其它的点,所以若u不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是S到所有的最短距离都确定下来,结束算法。若一个点入队次数超过n,则有负权环。
SPFA 在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的情况,k在2左右。
代码模板:
SPFA
void Spfa()
{
for (int i(0); i<num_town; ++i)//初始化
{
dis[i] = MAX;
visited[i] = false;
}
queue<int> Q;
dis[start] = 0;
visited[start] = true;
Q.push(start);
while (!Q.empty()){
int temp = Q.front();
Q.pop();
for (int i(0); i<num_town; ++i)
{
if (dis[temp] + road[temp][i] < dis[i])//存在负权的话,就需要创建一个COUNT数组,当某点的入队次数超过V(顶点数)返回。
{
dis[i] = dis[temp] + road[temp][i];
if (!visited[i])
{
Q.push(i);
visited[i] = true;
}
}
}
visited[temp] = false;
}
}