最短路径算法总结

其实这部分的内容我在之前的博客就记录了,今天闲来无事写来玩玩,没想到遇到这样那样的问题,自己去oj上提交一些代码居然还wa了几次,回顾一下吧

floyd算法
多源最短路径O(n^3)算法,原理很简单。由于复杂度高,所以n肯定不会多大,用邻接矩阵就够存了。

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int N=200;
int n,graph[N][N];
inline void floyd(){
    for(int k=1;k<=n;++k)
        for(int i=1;i<=n;++i)
            if(graph[i][k]!=INF)
                for(int j=1;j<=n;++j)
                    if(graph[i][j]>graph[i][k]+graph[k][j])
                        graph[i][j]=graph[i][k]+graph[k][j];
    for(int s=1;s<=n;++s){
        for(int i=1;i<=n;++i)
            if(s==i)cout<<0<<" ";
            else if(graph[s][i]!=INF)cout<<graph[s][i]<<" ";
            else cout<<-1<<" ";
        cout<<endl;
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;++i)
        for(int j=1;j<=n;++j){
            scanf("%d",&graph[i][j]);
            if(!graph[i][j])graph[i][j]=INF;
        }
    floyd();
}

dijkstra算法
单源最短路径算法,这是一种贪心的思维,贪心的策略是所有的点都有一个源点到改点的距离,每次都取距离最短的点,处理完这个点和邻接点以后不再处理该点,这一点用优先队列容易实现。

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int N=5e5+5;
struct edge{
    int from,to,w;
};
vector<edge>e[N];
struct s_node{
    int id,w;
    bool operator<(const s_node &s)const{return w>s.w;}
};
int n,m,s,dis[N],done[N];
inline void dijkstra(){
    for(int i=1;i<=n;++i)dis[i]=INF,done[i]=false;
    dis[s]=0;priority_queue<s_node>q;q.push({s,dis[s]});
    while(!q.empty()){
        s_node u=q.top();q.pop();
        if(done[u.id])continue;
        done[u.id]=true;
        for(int i=0;i<e[u.id].size();++i){
            edge y=e[u.id][i];
            if(done[y.to])continue;
            if(dis[y.to]>u.w+y.w){
                dis[y.to]=u.w+y.w;
                q.push({y.to,dis[y.to]});
            }
        }
    }

}
int main(){
    cin>>n>>m>>s;
    for(int i=1;i<=m;++i){
        int x,y,z;scanf("%d%d%d",&x,&y,&z);
        e[x].push_back({x,y,z});
    }
    dijkstra();
    for(int i=1;i<=n;++i)
        if(dis[i]!=INF)printf("%d ",dis[i]);
        else printf("%d ",(1<<31)-1);
}

Bellman_ford算法
这个算法的复杂度为O(n*m),但是这个算法有一个dijkstra无法实现的功能:判断负环。
之前提到的dijkstra是对点进行松弛操作,并且利用队列对其进行了优化。而bellman_ford则是对边进行松弛操作。假设有n个点,那么最多进行n-1次松弛操作(对每条边来说,就拿该边的终点边来做松弛操作)肯定就能把源点到所有点的最短距离求出来了。那么我们再进行第n次松弛操作的时候,假设还有一些点的距离还能更短,那一定是因为存在了负权边,只有这样才可能。

模板算法:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int INF=1e9;
struct edge{
    int from,to,w;
}e[5*N+5];
int dis[N],n,m,s;
inline bool bellman_ford(){
    for(int i=1;i<=n;++i)dis[i]=INF;dis[s]=0;
    for(int i=1;i<n;++i){
        bool flag=false;
        for(int j=1;j<=m;++j){
            if(dis[e[j].to]>dis[e[j].from]+e[j].w){
                dis[e[j].to]=dis[e[j].from]+e[j].w;
                flag=true;
            }
        }
        if(!flag)break;
    }
    for(int j=1;j<=m;++j)
        if(dis[e[j].to]>dis[e[j].from]+e[j].w)return true;//存在负权边
    return false;
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;++i)scanf("%d%d%d",&e[i].from,&e[i].to,&e[i].w);
    if(bellman_ford())cout<<"存在负权边"<<endl;
    else for(int i=1;i<=n;++i)printf("%d ",dis[i]);
}

SPFA算法
SPFA实际上是bellman_ford的队列优化算法,时间复杂度可以由之前的O(nm)优化到O(km),k是一个常量。
SPFA看似很无敌,但是在某些特定情况下也会被卡死,这点要注意一下。在最坏情况下,SPFA的算法复杂度和bellman_ford的O(n*m)是一样的。由于都是边松弛的算法,因此当然是边越少越好,也就是在面对稀疏图的时候,SPFA的效率是比较高的。

算法设计
(1)数据结构。由于是稀疏图,且点数很可能会给很大的时候我们会用SPFA,因此我们利用链式前向星来存储图,dis[i]记录从源点到节点i的最短路径长度,vis[i]标记节点i是否在队列中,sum[i]记录节点i入队次数(入队次数大于等于n的话一定有负环)。
(2)创建一个队列,源点u入队,标记u在队列中,u的入队次数加1。
(3)松弛操作。取出队头x,标记x不在队列中。考察x的所有出边i(x,v,w),如果dis[v]>dis[x]+e[i].w,则进行松弛。如果结点v不再队列中,则如果v的入队次数+1后大于等于n,则说明有负环,退出;否则v入队,标记v在队列中。
(4)重复松弛操作,直到队列为空。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int N=1e5+5;
struct edge{
    int to,w,next;
}e[5*N];
int head[N];//存储链式前向星中每个点的第一条边的编号
int n,m,s,cnt,sum[N],dis[N];
bool vis[N];
//vis数组用于标记是否在队列中,sum数组用于统计入队的次数
inline void add(int u,int v,int w){
    e[cnt].to=v,e[cnt].w=w;
    e[cnt].next=head[u];
    head[u]=cnt++;//头插,指向第cnt条边
}
inline bool spfa(){
    memset(vis,false,sizeof(vis));memset(sum,0,sizeof(sum));
    queue<int>q;
    for(int i=1;i<=n;++i)dis[i]=INF;
    dis[s]=0;sum[s]++;q.push(s);
    while(!q.empty()){
        int u=q.front();q.pop();vis[u]=false;
        for(int i=head[u];~i;i=e[i].next){
            int v=e[i].to;
            if(dis[v]>dis[u]+e[i].w){
                dis[v]=dis[u]+e[i].w;
                if(!vis[v]){
                    if(++sum[v]>=n)//存在负环
                        return true;
                    vis[v]=true;q.push(v);
                }
            }
        }
    }
    return false;
}
int main(){
    cin>>n>>m>>s;
    memset(head,-1,sizeof(head));
    for(int i=1;i<=m;++i){
        int x,y,z;scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
    }
    if(spfa())cout<<"存在负权边"<<endl;
    else
        for(int i=1;i<=n;++i)
            if(dis[i]!=INF)printf("%d ",dis[i]);
            else printf("%d ",(1<<31)-1);
}

算法优化:SLF
如果待入队的节点是j,队首元素为节点i,若dis[j]<dis[i],则将j插入队首,否则插入队尾。(用双端队列实现)。
这种方法在随机数据上表现优秀,但是在正权图上的最坏情况为O(nm),在负权图的最坏情况为达到指数级复杂度。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int N=1e5+5;
struct edge{
    int to,w,next;
}e[2*N];
int head[N];//存储链式前向星中每个点的第一条边的编号
int n,m,s,cnt,sum[N],dis[N];
bool vis[N];
//vis数组用于标记是否在队列中,sum数组用于统计入队的次数
inline void add(int u,int v,int w){
    e[cnt].to=v,e[cnt].w=w;
    e[cnt].next=head[u];
    head[u]=cnt++;//头插,指向第cnt条边
}
inline bool spfa(){
    memset(vis,false,sizeof(vis));memset(sum,0,sizeof(sum));
    deque<int>q;
    for(int i=1;i<=n;++i)dis[i]=INF;
    dis[s]=0;sum[s]++;q.push_front(s);
    while(!q.empty()){
        int u=q.front();q.pop_front();vis[u]=false;
        for(int i=head[u];~i;i=e[i].next){
            int v=e[i].to;
            if(dis[v]>dis[u]+e[i].w){
                dis[v]=dis[u]+e[i].w;
                if(!vis[v]){
                    if(++sum[v]>=n)//存在负环
                        return true;
                    vis[v]=true;
                    if(!q.empty()&&dis[q.front()]>dis[v])q.push_front(v);
                    //注意要判断队列是否为空
                    else q.push_back(v);
                }
            }
        }
    }
    return false;
}
int main(){
    cin>>n>>m>>s;
    memset(head,-1,sizeof(head));
    for(int i=1;i<=m;++i){
        int x,y,z;scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
    }
    if(spfa())cout<<"存在负权边"<<endl;
    else
        for(int i=1;i<=n;++i)
            if(dis[i]!=INF)printf("%d ",dis[i]);
            else printf("%d ",(1<<31)-1);
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值