第一种 Dijkstra算法(堆优化)
时间复杂度:无堆优化O(n^2),有堆优化O((m+n)logn)。
思想:用已经求出来的有最小值的节点松弛它所连的其他节点,即每次查找剩下所有节点中最小的一个用它松弛其他边
Dijkstra 每次循环都可以确定一个顶点的最短路径,故程序需要循环 n-1 次。
堆优化:(适用于边数远少于n^2的稀疏图)查找时用小根堆push一个节点和此节点新权值,并记录此节点查过(不删除此堆中节点以前权值,所以占空间较大(*10)),每次取堆顶看是否用过 若未用过用它去松弛其他节点,反之则跳过直到堆为空结束。
//无堆优化+邻接矩阵
//输入n和m,代表n个节点,m条边,然后是m行输入,每行有x,y,z,代表x到y的路距离为z。
//问题:从1出发到各点的最短路径。
#include<cstdio>
#include<cstring>
const int maxn=100;
int map[maxn][maxn];
int dis[maxn];
int path[maxn];
bool vis[maxn];
int n;
void dijk(int s)
{
memset(path,-1,sizeof(path));
memset(dis,0x3f,sizeof(dis)); //快速初始化为无穷大
memset(vis,0,sizeof(vis));
dis[s]=0;
while(1)
{
int k=0;
for(int j=1;j<=n;j++)
{
if(!vis[j]&&dis[j]<dis[k]) //这一步找未收录顶点中dis值最小的
k=j;
}
if(!k) return ; //没有未收录的顶点,则返回(注意顶点从1开始,k为0一定是没找到)
vis[k]=1; //收录顶点k
for(int j=1;j<=n;j++)
{
if(dis[j]>dis[k]+map[k][j])
{
dis[j]=dis[k]+map[k][j];
path[j]=k;
}
}
}
}
void print(int x)
{
if(x==-1) return ;
print(path[x]);
printf("%d->",x);
}
int main()
{
int m,x,y,z,order;
scanf("%d%d",&n,&m);
memset(map,0x3f,sizeof(map));
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&x,&y,&z);
map[x][y]=map[y][x]=z;
}
dijk(1);
scanf("%d",&order);
printf("%d
",dis[order]);
print(path[order]);
printf("%d",order);
return 0;
}
//eg:洛谷P3371
//堆优化+邻接表
#include<cstdio>
#include<iostream>
using namespace std;
const int INF=2147483647;
int n,m,st,cnt,minn=INF,siz;
int head[10001],dis[10001],vis[100001];
struct edge{
int to,next,mes;
}edge[500001];
struct hp{
int dis,nu;
}hp[100001];
void add(int x,int y,int z){
edge[++cnt].next=head[x];
edge[cnt].to=y;
edge[cnt].mes=z;
head[x]=cnt;
}
void push(int x,int y){
siz++;
hp[siz].nu=x,hp[siz].dis=y;
int now=siz;
while(now>1){
if(dis[hp[now].nu]<dis[hp[now/2].nu]){
swap(hp[now],hp[now/2]);
now=now/2;
}
else break;
}
}
int del(){
int res=hp[1].nu;
hp[1]=hp[siz--];
int now=1;
while(2*now<=siz){
int tp=2*now;
if(tp<siz&&dis[hp[tp].dis]>dis[hp[tp+1].nu]) tp++;
if(dis[hp[now].nu]>dis[hp[tp].nu]){
swap(hp[now],hp[tp]);
now=tp;
}
else break;
}
return res;
}
void dij(int st){
for(int i=1;i<=n;i++) dis[i]=INF;
dis[st]=0;
push(st,dis[st]);
int now=st;
while(siz){
now=del();
if(vis[now]) continue;
vis[now]=false;
for(int i=head[now];i;i=edge[i].next){
int mu=edge[i].to;
if(dis[mu]>dis[now]+edge[i].mes){
dis[mu]=dis[now]+edge[i].mes;
push(mu,dis[mu]);
}
}
}
}
int main(){
scanf("%d %d %d",&n,&m,&st);
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
add(x,y,z);
}
dij(st);
for(int i=1;i<=n;i++) printf("%d ",dis[i]);
return 0;
}
第二种 SPFA算法
时间复杂度:O(KE),E为边数,K为常数
最差时间复杂度:O(n^3),即O(nE)。
特点:在 Bellman-ford 算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。,可以判断有无负权回路(若有,则不存在最短路)。
而其检测负权回路的方法也很简单,如果某个点进入队列的次数大于等于 n,则存在负权回路,其中 n 为图的顶点数。
//eg:洛谷P1828
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int INF=0x3f3f3f3f;
int n,m,cnt,minn=INF,p,c;
int que[4010],dis[801],hea[801],place[801];
bool flag[801];
struct edge{
int to,next,mes;
}edge[4010];
void add_n(int x,int y,int z){
edge[++cnt].next=hea[x];
edge[cnt].to=y;
edge[cnt].mes=z;
hea[x]=cnt;
}
void spfa(int t){
memset(flag,false,sizeof flag);
memset(dis,INF,sizeof dis);
int head=1,tail=1;
dis[t]=0;
que[head]=t;
flag[t]=true;
while(head<=tail){
for(int i=hea[head[que]];i;i=edge[i].next){
if(dis[edge[i].to]>dis[que[head]]+edge[i].mes){
dis[edge[i].to]=dis[que[head]]+edge[i].mes;
if(!flag[edge[i].to])
que[++tail]=edge[i].to;
flag[edge[i].to]=true;
}
}
flag[que[head]]=false;
head++;
}
int sum=0;
for(int i=1;i<=n;i++) sum+=dis[place[i]];
if(sum<minn) minn=sum;
}
int main()
{
// freopen("testdata.txt","r",stdin);
scanf("%d %d %d",&n,&p,&c);
for(int i=1;i<=n;i++)
scanf("%d",&place[i]);
for(int i=1;i<=c;i++){
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
add_n(x,y,z);
add_n(y,x,z);
}
for(int i=1;i<=p;i++)
spfa(i);
printf("%d",minn);
return 0;
}
第三种: Floyd算法
时间复杂度:O(n^3)
思想:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:依次扫描每一点(k),并以该点作为中介点,计算出通过k点的其他任意两点(i,j)的最短距离。
步骤:
1、用D[v][w]记录每一对顶点的最短距离。
2、依次扫描每一个点,并以其为基点再遍历所有每一对顶点D[][]的值,看看是否可用过该基点让这对顶点间的距离更小。
#include<stdio.h>
#include<iostream>
#include<limits.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int INF=INT_MAX/100;
int d[3000][3000],n,m;
void floyed()
{
for(int k=0; k<n; k++) //插入k点
{
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
if(d[i][k]<INF&&d[k][j]<INF) //消除加法溢出问题
d[i][j]=min(d[i][j],d[i][k]+d[k][j]); //更新两点距离
}
}
}
}
void init()
{
cin>>n>>m;
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
if(i==j)
d[i][j]=0;
else
d[i][j]=INF;
}
for(int i=0; i<m; i++)
{
int sta,stb,coc;
cin>>sta>>stb>>coc;
d[sta][stb]=coc;
}
}
int main()
{
init();
floyed();
return 0;
}
算法类比:
dj和ford算法用于解决单源最短路径,而floyd算法解决多源最短路径。
dj适用稠密图(邻接矩阵),因为稠密图问题与顶点关系密切;
ford和dj+堆优化算法适用稀疏图(邻接表),因为稀疏图问题与边关系密切。
floyd在稠密图(邻接矩阵)和稀疏图(邻接表)中都可以使用;
PS:dj算法虽然一般用邻接矩阵实现,但也可以用邻接表实现,只不过比较繁琐。而ford算法只能用邻接表实现。
dj算法不能解决含有负权边的图;
而Floyd算法和Ford算法可以解决含有负权边的问题,但都要求没有总和小于0的负权环路(需要注意的是,如果是无向图,只要存在一条负边,该图就存在负权回路)。
SPFA算法可以解决负权边的问题,而且复杂度比Ford低。形式上,它可以看做是dj算法和BFS算法的结合。
3种算法都是既能处理无向图问题,也能处理有向图问题。因为无向图只是有向图的特例,它们只是在邻接矩阵或邻接表的初始化时有所区别。