目录
一、Dijkstra算法:
仅限用于无边权为负数的图,更不能正确处理负环的情况。
把所有点分成两个集合,S集合是存已经确定下来、不会再更改(准确一点的话是“不能再更改”)的点,T集合是待确定的点集。
从T集合中的点中,选择一个到st最近的点x。让这个点为其它点更新:如果y点到st的距离大于 x点到st的距离 + x->y这条边的距离,则改进y到st的值。然后x加入S集合。
更透彻地理解,这种算法每次循环都会确定下一个点,这个点到st的距离一定是最优解中它到st的距离。理由是它是目前距离最小的点之一,边权都是正的,对于后面的点到st的距离相对于当前的最小点来说只会加而不会减,所以它无法再有更小的值了。借着这一点贪心,我们就可以通过n(点的个数)次上面这样的操作,十分稳定地求出全图各点到st的最短距离。
Dijkstra的算法可以用堆优化,其实就是开一个priority_queue(优先队列),来存储和快速提取dis里的最小值。可以由O(n^2)优化到O(nlogn)。
堆优化的dijkstra代码见下:
#include<queue>
#include<cstdio>
#include<cstring>
#include<utility>
#include<algorithm>
using namespace std;
typedef pair<int,int> pii;//pii(边权,点编号)
const int inf=1061109567;
const int maxn=10010,maxm=500010;
int n,m,s;
struct edge
{
int x,y,c,next;
}e[maxm];int len=0,last[maxn];
void ins(int x,int y,int c)
{
e[++len]=(edge){x,y,c,last[x]};last[x]=len;
}
int dis[maxn];
bool v[maxn];
priority_queue<pii,vector<pii>,greater<pii> > q;
void dijkstra()
{
memset(dis,63,sizeof(dis));dis[s]=0;
int cnt=0;
q.push(pii(0,s));
while(!q.empty())
{
int x=q.top().second,d=q.top().first;q.pop();
if(v[x]) continue;
dis[x]=d;v[x]=true;
cnt++;
for(int k=last[x];k;k=e[k].next)
{
int y=e[k].y;
if(dis[y]>dis[x]+e[k].c)
{
dis[y]=dis[x]+e[k].c;
q.push(pii(dis[y],y));
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++)
{
int x,y,c;
scanf("%d%d%d",&x,&y,&c);
ins(x,y,c);
}
dijkstra();
for(int i=1;i<=n;i++)
{
if(dis[i]==inf) printf("2147483647 ");
else printf("%d ",dis[i]);
}
return 0;
}
二、Spfa算法:
可以用于带负边权的图,也可以用来判断图中有无负环。
用于判断负环的方法是:再标准spfa模版下,如果有一个点进队列的次数达到n次,则存在负环。
spfa可以用SLF和LLL来优化,用得好可以减少一半左右时间。
由于spfa较于简单,此处不再详讲。
三、Floyd算法:
此算法可以求any point(任意一点)到any other point(其它任意一点)的最短距离。时间复杂度O(n^3)
floyd其实是一个DP算法,它先枚举一个交换点,然后看看哪对经过这个点的距离会比原来要短。一开始的时候只有1做交换点,此时的f[x][y]仅表示可以经过1号店后的最短距离。接下来又允许经过2号点,求出了允许经过1、2号点的最短距离。……到n号点时,就求出了可以经过1~n号点的最短距离,此时的f[x][y]就是这张图中两两间的最短距离了。它的DP方程完整为f[k][i][j]=f[k-1][i][k]+f[k-1][k][j](其中k为中转点,f[k][i][j]表示可以把1~k作为中转点时i~j的最短距离)。其中第一维的k没有意义,可以删掉。
floyd保存路径的方法要只需要一句话,但背后的原理还是不浅的。想要达到目的,可以用前缀、后缀、保存中转点等多种方法,愚认为用前缀最好了。
设path[i][j]表示从i->j这条路径上离开i后的下一个点的坐标,这样我们就可以通过下面这段代码输出路径。它的循环就是为了不断找寻当前路径上的次靠前的那个点,再借助那个点来求下一步路径。例如有这么一条路径:4->5->3->1,则path[4][1]=5,path[5][1]=3,path[3][1]=1,这样一开头再加上4就成了一条完整的路径了。
while(1)
{
int x,y;
scanf("%d%d",&x,&y);
if(path[x][y]==-1)
{
printf("No Way\n");
}
else
{
printf("dis: %d\nway: ",ma[x][y]);
while(x!=y)
{
printf("%d -> ",x);
x=path[x][y];
}
printf("%d\n\n",y);
}
}
在更改时,也要啃住前缀来更新。如果从i出发去j,途经k后最短,其路径为:i->…->k->…->j。如果想要得到完整路径,可以分别让path[i][k]和path[k][j]递归得到。若要记录下来,我们也只是需要记录下当前路径下从i出发之后的点的编号即可,即path[i][k]。接下来从(设q=path[i][k])q的下一步的点的编号,会在path[q][y]中记录,这是在求q到y的最短路时的事情,它的记录必然会是路径i->q->…->k->…->j的一部分。
以下是floyd+路径储存的代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=100;
int ma[maxn][maxn],path[maxn][maxn];
int main()
{
memset(ma,63,sizeof(ma));
memset(path,-1,sizeof(path));
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
ma[i][i]=0;
path[i][i]=i;
}
for(int i=1;i<=m;i++)
{
int x,y,c;
scanf("%d%d%d",&x,&y,&c);
ma[x][y]=c;
path[x][y]=y;
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(ma[i][j]>ma[i][k]+ma[k][j])
{
ma[i][j]=ma[i][k]+ma[k][j];
path[i][j]=path[i][k];
}
}
}
}
while(1)
{
int x,y;
scanf("%d%d",&x,&y);
if(path[x][y]==-1)
{
printf("No Way\n");
}
else
{
printf("dis: %d\nway: ",ma[x][y]);
while(x!=y)
{
printf("%d -> ",x);
x=path[x][y];
}
printf("%d\n\n",y);
}
}
return 0;
}
四、DP记忆化搜索
这种方法的限制很多,它必须是无环图,包括不能有双向边。它在应用于无单一源点或汇点,不要求全图连通,但在每个连通块里面边的方向总体一致的图中,或者是求每个点到最近汇点的距离,能达到O(n)的时间复杂度。
每次操作时,随机选取一个没有遍历过的点,让它去搜寻最短路,搜完后记录下来,为后人提供方便。
代码很好理解,就不多写了:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=1061109567;
const int maxn=100;
const int maxm=100;
int d[maxn];
struct node
{
int x,y,c,next;
}a[maxm];int len=0,last[maxn];
void ins(int x,int y,int c)
{
len++;
a[len].x=x;a[len].y=y;a[len].c=c;
a[len].next=last[x];last[x]=len;
}
void dfs(int x)
{
if(d[x]!=inf) return ;
if(last[x]==0) d[x]=0;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
dfs(y);
if(d[x]>d[y]+a[k].c) d[x]=d[y]+a[k].c;
}
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y,c;
scanf("%d%d%d",&x,&y,&c);
ins(x,y,c);
}
memset(d,63,sizeof(d));
for(int i=1;i<=n;i++)
{
if(d[i]==inf) dfs(i);
}
for(int i=1;i<=n;i++) printf("%d: %d\n",i,d[i]);
return 0;
}
五、线段树求最短路
对于一棵每个节点都有直接回到根节点的树,先求出各个点到根节点的最短距离,用线段树来维护。要求各个点的距离时,利用和差可以求到各个点间的最短路径。
具体的内容请往这看。
终、对比与分析
最后再来比较一下以上几种最短路算法:
在功能方面:
能判断负环:spfa
多个点到某个点的最短距离(单源最短路):dijkstra,spfa,DP记忆化搜索
任意两点间最短距离(多源最短路)(任意图):floyd
树上任意两点间最短距离:floyd,(dijkstra,spfa,DP记忆化搜索 要多一个是否在一棵子树上的判断)
带正环的图:floyd也可以,但DP记忆化搜索不行
在时间方面:
稠密图dijkstra,稀疏图spfa