松弛操作:当dis[i] > dis[j] + g[j][i]时 dis[i] = dis[j] + g[j][i]
dijkstra
单源最短路 O(n^2)
本质是贪心 不能处理负边
分成两个集合 用vis标记 一个是已经找到最短路的一个是没有找到最短路的
从已经找到最短路里的点出发,进行松弛操作 找到其中最小的加入最短路的集合中 再从这个点接下去更新,做n-1次
所有的距离初始化为inf
#include<stdio.h>
const int N=100;
const int INF=100000; //INF假定为无穷大
int p[N][N],d[N]; //p表示各节点间的距离,不存在路径即为无穷大;d表示从出发节点到各节点的最短路径长度
void dijkstra(int sec,int n) //sec为出发节点,n为图中的节点数
{
int i,j,min,min_num;
int vis[N]={0,}; //用于标记是否已作为过中途节点,0表示没有,1表示有
for(i=0;i<n;i++) //初始化
{
d[i]=p[sec][i];
}
vis[sec]=1;d[sec]=0; //出发节点到自己的距离永远为0
for(i=1;i<n;i++)
{
min=INF;
for(j=0;j<n;j++) //每次循环取出d数组中的未被作为过中途节点且数值最小的
{
if(!vis[j]&&d[j]<min)
{
min=d[j]; //更新最小值
min_num=j; //更新最小值所对应的节点,即记录下标
}
}
vis[min_num]=1; //标记该节点,表示其已被作为中途节点
for(j=0;j<n;j++) //循环,经过min_num节点到达是否有更小距离,如有更小距离则更新d数组
{
if(d[j]>min+p[min_num][j])
{
d[j]=min+p[min_num][j];
}
}
}
}
int main()
{
int i,j,n=5; //n表示图中的节点个数
for(i=0;i<n;i++) //程序用二维数组p存储各节点间的距离,这里则进行初始化
{
for(j=0;j<n;j++)
{
p[i][j]=(i==j?0:INF); //初始化:i到j路径为无穷大或者i到i本身为0
}
}
p[0][1]=10;p[0][3]=30;p[0][4]=100;p[1][2]=50;p[2][4]=50;p[3][2]=20;p[3][4]=60; //p[i][j]表示节点i到节点j的距离
dijkstra(0,n); //求从节点0出发到各节点的最短距离
for(i=0;i<n;i++) //打印从节点0出发到各节点的最短距离
{
printf(i==n-1?"%d
":"%d ",d[i]);
}
return 0;
}
记录路径:通过path数组来记录路径,path[i]=j表明节点i取得最小路径时,其最后一段走的是节点j到节点i。然后用栈回溯
可以使用优先队列优化dijkstra, 让权值小的先出列,自定义比较器。使用pair 将结点和权值一起存入队列
Bellman-Ford
单源最短路 可以处理负边 O(mn)
初始:dist(1)[u]=edge[v0,u],v0是源点
递推:dist(k)[u]=min{dist(k-1)[u],min{dist(k-1)[j]+edge[j,u]}} j=0,1,…,n-1,j<>u; k=2,3,4,…,n-1 。
dist(i)[u]为从源点v0出发最多经过不构成负权值回路的i条边到达终点u的最短路径长度
n-1次过后便可以得到最短路,如果n-1次后依旧可以更新,那么说明存在负权回路或者是正权回路。
Bellman-ford算法有一个小优化:每次松弛先设一个标识flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出
for(int k=1;k<=n-1;k++)//遍历点的次数
{
for(int i=1;i<=m;i++)//遍历边的次数
{
if(dis[v[i]]>dis[u[i]]+w[i])//如果从u到v的距离能够通过w这条边压缩路径 就要进行松弛操作
{
dis[v[i]]=dis[u[i]]+w[i];
}
}
}
bellman的队列优化
算法思想:我们用数组d记录每个结点的最短路径估计值,用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止
期望的时间复杂度O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
void spfa(int s)
{
for(int i = 1; i <= n; i++){
dis[i] = inf;
vis[i] = false;
}
dis[s] = 0;
vis[s] = true;
queue<int> que;
que.push(s);
int i, v;
while(!que.empty()){
v = que.front();que.pop();
vis[v] = false;
for(i = 1; i <= n; i++){
if(a[v][i] > 0 && dis[i] > dis[v] + a[v][i]){
dis[i] = dis[v] + a[v][i];
if(vis[i] == 0){
que.push(i);
vis[i] = true;
}
}
}
}
}
- SPFA算法有两个优化算法 SLF 和 LLL:
- SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,
- 否则插入队尾。
- LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入
- 到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。
Floyd
任意两点最短路 O(n^3) 可处理负边
对于稀疏的图,采用于
茂密的图,可以使用
Floyd
算法。
对于稀疏的图,采用n次Dijkstra比较出色,对于茂密的图,可以使用Floyd算法。i到j的最短距离不外乎存在经过i与j之间的k和不经过k两种可能,所以可以令k=1,2,3,...,n(n是城市的数目),在检查d(ij)与d(ik)+d(kj)的值;在此d(ik)与d(kj)分别是目前为止所知道的i到k与k到j的最短距离,因此d(ik)+d(kj)就是i到j经过k的最短距离。所以,若有d(ij)>d(ik)+d(kj),就表示从i出发经过k再到j的距离要比原来的i到j距离短,自然把i到j的d(ij)重写为d(ik)+d(kj),每当一个k查完了,d(ij)就是目前的i到j的最短距离。重复这一过程,最后当查完所有的k时,d(ij)里面存放的就是i到j之间的最短距离了。
for(int k=0; k<n; k++)//k一定要在最外层
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
这样作的意义在于固定了k,把所有i到j而经过k的距离找出来,然后象开头所提到的那样进行比较和重写,因为k是在最外层的,所以会把所有的i到j都处理完后,才会移动到下一个k,这样就不会有问题了
输出路径:
p(ij)的值如果为p,就表示i到j的最短行经为i->...->p->j,也就是说p是i到j的最短行径中的j之前的最后一个城市
一旦发现d(ij)>d(ik)+d(kj),就把p(kj)存入p(ij)
n
次tra,对于
茂密的图,可以使用
Floyd
算法。
小技巧:
稀疏图使用邻接表存储