图的最短路

第一种 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种算法都是既能处理无向图问题,也能处理有向图问题。因为无向图只是有向图的特例,它们只是在邻接矩阵或邻接表的初始化时有所区别。

版权声明:本文为博主原创文章,未经博主允许不得转载。 博主:https://www.cnblogs.com/Menteur-Hxy/
原文地址:https://www.cnblogs.com/Menteur-Hxy/p/9248059.html