图论之最短路径问题(朴素Dijksra算法\堆优化版Dijksra算法\Bellman-Ford\SPFA)

朴素Dijskra算法

时间复杂度:O(n^{2}),适用于稠密图,需要用邻接矩阵来存储。

算法描述

设起点为s,dist[v] 表示起点到v点的最短距离。
a)初始化 dist[v]=INF(v!=s),dist[s] = 0 

这里一共有n个点,第一个点(起点)初始化为0,其余都初始化为inf。

for(int i=1;i<=n;i++)对一个一个点进行遍历

1.在没有被访问过的节点中,找一个顶点u,使得dist[u]是最小的

2.u标记为已经确定的最短路径

3.For与u相连的每一个未确定的最短路径节点v

if(dist[u]+w[u][v] < dist[v]){

        dist[v] = dist[u]+w[u][v];

}

算法结束。

图例

假如有以下图:

初始化:

第一轮:

中间场:

此时要再dist数组中找到一个具体起点最短的点,这里是2号点,要把2好点设置为已经确定是最小路径了(如何证明?百度自己搜doge)。

第二轮:

接着遍历与2号节点相连的并且没有确定最短路径的点(这里是3、5),这里因为d[2]+w[2][3]<d[3]。d[2]+w[2][5]<d[5]。所以更新3和5号点。

中间场:

此时还未确定最短路径的还有3、4、5号点,最小的是3号点。将3号点距离确定为最小。

第三轮:

查看与3好点相连的还没有确定最短路径的点。即4、5号点,更新了4号点。

中间场:

再剩下的没有确定最短路径的点中找到最小的,这里是5号点,将其设置为确定状态。

第四轮:

找到剩余没有确定最短路径的点,即4号点,将其设置为最短的。

算法结束。

例题:

第一行包含整数 n和 m。

接下来 mm 行每行包含三个整数 x,y,z表示存在一条从点 x到点 y 的有向边,边长为 z。

输出格式:

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

#include<iostream>
#include<cstring>
using namespace std;
const int N = 510;
int g[N][N];//图的邻接矩阵
int dist[N];//每个点距离起点的最短距离
bool st[N];//每个点是否已经确定最短距离
int n,m;

int dijkstra(int start){
    memset(dist,0x3f,sizeof(dist));
    dist[start] = 0;
    //st[start] = true;这个不需要,因为后面是从1号遍历到n号,所以第一次选没确定的要选上起点
    for(int i=0;i<n;i++)
    {
        int t = -1;//t来存储剩余的没有确定最短轮径的最小的节点。
        for(int j=1;j<=n;j++)
        {
            if(!st[j] && (t==-1 ||dist[t]>dist[j]))
            {        //并上t==-1是如果t==-1就不执行后面的了
                t = j; //因为在一个集合中,我们不能确定哪一个点还没有被确定最短路径,所以让他自己遍历,遍历到第一个就是t的值
            }
        }
        st[t] = true;
        
        for(int j=1 ; j<=n ;j++)
        {
            if(dist[j]>dist[t] + g[t][j])
            {
                dist[j] = dist[t] + g[t][j];
            }
        }
    }
    if(dist[n]==0x3f3f3f3f)
    {
        return -1;
    }
    else
    {
        return dist[n];
    }
}

int main(){
    scanf("%d%d",&n,&m);
    memset(g,0x3f,sizeof(g));
    for(int i=0;i<m;i++){//存储图
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b] = min(g[a][b],c);//有多条边,只要保存长度最小的那一条。
        
    }
    
    int res = dijkstra(1);
    cout<<res;
    return 0;
}



堆优化版Dijskra算法

时间复杂度:O(mlog(n)),适用于稠密图,用邻接表来存储。

优化点:此时点数很多,如果载用邻接矩阵存储显然不现实,所以需要用邻接表。上面朴素版的是直接循环的,在第二层循环中找到还没有确定最短路径的最小的点是O(n)。如果用优先队列则只需要O(1)。只是建立和维护队列需要O(logn)。

#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 150009;
int h[N],e[N],ne[N],w[N],dist[N],idx,n,m;
bool st[N];
void add(int a,int b,int c){
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx] = c;
    idx++;
}

int dijkstra(int start){
    memset(dist,0x3f,sizeof(dist));
    dist[start] = 0;
    
    priority_queue<PII,vector<PII>,greater<PII> > heap;
    heap.push({0,start});//这里把编号放在第二位,因为默认比较的时候是比较pari的第一位。
    while(!heap.empty()){
        PII t = heap.top();
        heap.pop();
        int ver = t.second,distance = t.first;
        if(st[ver]) continue;
        st[ver] = true;
        for(int i=h[ver];i!=-1;i=ne[i]){
            int j = e[i];
            if(dist[j]>distance+w[i]){
                dist[j] = distance+w[i];
                heap.push({dist[j],j});
            }
        }
        
    }
    
    if(dist[n]==0x3f3f3f3f)
    {
        return -1;
    }
    else
    {
        return dist[n];
    }
}

int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    for(int i=0;i<m;i++){//存储图
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    int res = dijkstra(1);
    cout<<res;
    return 0;
}



Bellman-Ford

时间复杂度:O(nm)

可以解决存在负全边的最小路径问题。

算法描述:

//设置s为起点,dist[v]为s到v的最短距离,pre[v]为v的前驱。w[j]为边j的长度,且j连接u、v。
//初始化:dist[s]=0,dist[v]=inf(v!=s),pre[s]=0
//For(i = 1;i<n;i++)
//For(j=1;j<=M;j++)
//  if(dist[u[j]] + w[j] < dist[v[j]])    u[j]、v[j]分别是这条边连接的两个起点和终点
//  {
//      dist[v[j]] = dist[u[j]] + w[j];
//      pre[v[j]] = u[j];
//  }

图例:

假如给出这些边:

2         3         2

1         2         -3

1         5         5

4         5         2

3         4         3

第一轮第一次遍历边:

不用更新

第一轮第二次遍历边:

更新节点2的dist

第一轮第三次遍历边:

更新节点5的dist

第一轮第四次遍历边:

不满足条件。

第一轮第五次遍历边:

不满足条件

第二轮第一次遍历边:

更新3号节点dist

...

最后会得到最短路径。

853. 有边数限制的最短路 - AcWing题库

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510,M=10009;
int n,m,k;
struct Edge{
    int a,b,w;
}edgs[M];

int dist[N],copydist[N];

int Bellman_Ford(int start){
    memset(dist,0x3f,sizeof(dist));
    dist[start] = 0;
    for(int i=0;i<k;i++){这里最多循环到k是因为要求:最多经过 k条边的最短距离,这就只需要最多k次就可以完成。
        memcpy(copydist,dist,sizeof(dist));//这里备份的原因是因为最长的是k,如果一层循环中在刚刚更新的基础上再次循环边,就可能出现超过k的情况
        for(int j=1;j<=m;j++){
            int a = edgs[j].a,b=edgs[j].b,w=edgs[j].w;//注意这里的a、b是节点编号,w是这条边的权重
            dist[b] = min(copydist[a]+w,dist[b]);//注意这里是用原来的dist即备份过的copydist来更新此次的dist
            
        }
    }
    
    if(dist[n]> 0x3f3f3f3f /10) return 0;
    else return dist[n];
    
}


int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=m;i++){
        int a1,b1,c1;
        scanf("%d%d%d",&a1,&b1,&c1);
        edgs[i] = {a1,b1,c1};
    }
    
    int t = Bellman_Ford(1);
    if(t==0) printf("impossible");
    else{
        printf("%d",t);
    }
    
    return 0;
}

SPFA

时间复杂度:一般O(m),最坏:O(mn)

正常情况下Bellman-Ford算法(不是限制了边数),最主要的就是更新dist。

即:dist[b] = min(dist[a]+w,dist[b]);

我们仔细想一下,好像只有b的前驱a变小了,那么b才有可能变小。

那么我们可以用一个队列来存储所有“变小”的节点,然后每次再从队列中拿出这个点,只更新这个点的指向的边。

算法描述:

queue<--1

while queue非空

1.  t<--q.front();

     q.pop();

2.更新t的所有出边 t-->b

    queue<--b

851. spfa求最短路 - AcWing题库

这里初始化dist为最大值时有点问题,先自定义一个INF然后用fill这样安全一点。

如果初始化0或者-1可以直接用memset。

#include<iostream>
#include<queue>
#include<cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
const int INF = 1000000000;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N];//用来判断这个点是否已经在队列中

void add(int a,int b,int c){
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx] = c;
    idx++;
}

int spfa(int start){
    fill(dist,dist+N,INF);
    dist[start] = 0;
    queue<int> q;
    q.push(start);
    st[start] = true;
    while(!q.empty()){
        int t = q.front();
        q.pop();
        st[t] = false;
        for(int i = h[t];i != -1 ;i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    if(dist[n] == INF) return 0;
    return dist[n];
}

int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    for(int i=0;i<m;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    int res = spfa(1);
    if(res == 0) printf("impossible");
    else{
        printf("%d",res);
    }
    
    return 0;
}

判断是否存在负环

我们的SPFA算法也可以判断一个图中是否存在负数环。

求解思想如下:

我们可以增加一个数组来记录当前节点距离起点的边数,我们每次更新dist数组时有dist[t]=dist[j]+w[i],此时我们也更新cnt[t] = cnt[j] + 1;这里cnt[v]存储的是点v到起点的最短路径的边的数量。又因为只要有负数边,那么就一定会更新,一个整数加一个负数一定小于原来的数。因为原点1不一定可以到达负边的圈,所以需要把所有点都入队。

#include<iostream>
#include<queue>
#include<cstring>
#include <algorithm>
using namespace std;
const int N = 2009,M=10009;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dist[N];
bool st[N];
int cnt[N];

void add(int a,int b,int c){
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    w[idx] = c;
    idx++;
}

int spfa(){
    queue<int> q;
    for(int i=1;i<=n;i++){
        q.push(i);
        st[i] = true;
    }
    while(!q.empty()){
        int t = q.front();
        q.pop();
        st[t] = false;
        for(int i = h[t];i != -1 ;i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] +1;
                if(cnt[j]>=n) return 1;
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return 0;
}

int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    for(int i=0;i<m;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    int res = spfa();
    if(res) printf("Yes\n");
    else{
        printf("No");
    }
    
    return 0;
}

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

背水

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

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

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

打赏作者

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

抵扣说明:

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

余额充值