最短路专题

最短路

在这篇文章中,n代表点的数量 ,m代表边的数量。

单源无负权最短路

戴克斯特拉

适用于稠密图,且图中无负权的情况 。
时间复杂度:O(n2)

#include <iostream> 
#include <cstdio>
#include <cstring>
using namespace std ;
const int N = 505 ;
const int inf = 0x3f3f3f3f ;

int W[N][N] ;
int d[N] ;
bool vis[N] ;

void dijkstra(int n){
    d[1] = 0 ;
    for(int i = 1 ; i <= n ; i ++ ){
        int minn = inf , k = 0 ;
        for(int j = 1 ; j <= n ; j ++ ){
            if(d[j] < minn && !vis[j] ){
                minn = d[j] ;
                k = j ;
            }
        }
        vis[k] = true ;
        for(int j = 1 ; j <= n ; j ++){
            if( d[j] > d[k] + W[k][j]) {
                d[j] = d[k] + W[k][j] ;
            }
        }
    }
}

int main(){
    memset(W , 0x3f , sizeof W) ;
    memset(d , 0x3f , sizeof d);
    int  n , m ;
    cin >> n >> m ;
    for(int i = 1 ; i <= m ; i ++){
        int a ,b ,c;
        cin >> a >> b >> c ;
        W[a][b] = min(W[a][b] , c ) ;
    }
	for(int i = 1 ; i <= n ; i ++){
	        W[i][i] = 0 ;
	    }
    dijkstra(n) ;
    
    if(d[n] == inf ) puts("-1"); 
    else cout << d[n] << endl ;
    
    return 0 ;
}

堆优化戴克斯特拉

适用于稀疏图 ,且图中没有负权边的情况。
时间复杂度:O(m logn)
但在稠密图的情况下,m趋近于n2,回导致时间复杂度成为O(n2 logn),反而没有朴素dijkstra算法高校。

此算法的思想主要是优化在朴素版中的寻找最小d的过程。

我们采用小根堆实现一个优化过程,让每一次有值更新时都采取入对并且对堆造成影响。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 150005 ;

typedef pair <int ,int > PII ;

int h[N] , e[N] ,w[N] , ne[N] , idx ;
int d[N] ;
bool vis[N] ;

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


void dijkstra() {
    memset(d , 0x3f ,sizeof d);
    priority_queue<PII ,vector<PII> ,greater<PII> > q;
    q.push({0 , 1});
    d[1] = 0 ;
    
    while(q.size()){
        auto t = q.top();
        q.pop();
        
        int ver = t.second , ww = t.first ;
        if(vis[ver]) continue ;
        vis[ver] = true ;
        
        for(int i = h[ver] ; i != -1 ; i = ne[i] ){
            int jv = e[i] , jw = w[i] ;
            if(!vis[jv] && d[jv] > jw + ww){
                d[jv] = jw + ww ;
                q.push({d[jv] , jv}) ;
            }
        }
    }
}


int main(){
    memset(h , -1 , sizeof h ) ;
    int n ,m ;
    cin >> n >> m ;
    for(int i = 1 ; i <= m ; i ++){
        int a , b , c ;
        cin >> a >> b >> c ;
        add(a ,b ,c) ;
    }
    
    dijkstra();
    
    if (d[n] == 0x3f3f3f3f ) puts("-1" ) ;
    else cout << d[n] << endl ;
    
    return 0 ;
}

单源有负权最短路

Bellman-Ford

在有负权边的情况下很有可能求不出最短路:
负权回路
若从起点到终点上存在负权回路,我们是无法求出最短路径的。
负权回路:此回路上的权值总和小于0

但如果负权回路不在起点与终点的路径上,我们是可以求出最短路径的。
负权回路

bellman-ford算法才去的思想主要是动态规划。
算法中的迭代k次代表的是最多不超过k条边。
有一个注意的点是最后判断某个点(n)是否能够到达不能直接判断d[n] == 0x3f3f3f3f ,原因是0x3f3f3f3f并不是数学意义上的无穷,所依某个并不能到达的点可能会被更新。所依我们要判断的条件是是否大于0x3f3f3f3f-m*(权值下限)
d [ n ] > i n f − m × 权 值 下 限 d[n] > inf - m \times 权值下限 d[n]>infm×

时间复杂度: O(nm)
使用场景:有负权边且求最多不超过k条边的最短路径问题,若SPFA被卡也可尝试这个。

#include <iostream>
#include <cstring>
using namespace std ;
const int N = 100005 ;

typedef struct {
    int a,b,w;
}Node ;

Node edge[N] ;
int d[505] ,backup[505];

void Bellman_Ford(int m , int k){
    memset(d , 0x3f , sizeof d) ;
    d[1] = 0 ;
    for(int i = 1 ; i <= k ; i ++ ){
        memcpy(backup , d , sizeof d) ;//数据污染,所以要将未被污染的copy
        for(int j = 1 ; j <= m ; j ++){
            int a = edge[j].a , b = edge[j].b , w = edge[j].w ;
            d[b] = min (d[b] , backup[a] + w) ;
        }
    }
}



int main(){
    int n ,m ,k ;
    cin >> n >> m >> k ;
    for (int i = 1 ; i <= m ; i ++){
        int a, b ,c;
        cin >> a >> b >> c ;
        edge[i] = {a , b , c};
    }
    
    Bellman_Ford(m,k);
    
    
    if (d[n] > 0x3f3f3f3f / 2) puts("impossible");
    else cout << d[n] << endl ;
    
    return 0 ;
}

SPFA

SPFA算法的是利用队列将bellman-ford算法进行的优化。在队列中的点(除了起始点是一开始就入队)都是因为这个点发生了更新,我们才让他入队。
也就是说,我们每次取队头,遍历他的边,若与之相连的点发生了更新,我们就让这个点入队。

时间复杂度 :一般O(m) ,最坏O(nm)
适用场景:有负权边的最短路径问题,判断负环。

最短路径问题
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 100005;

int h[N] ,e[N] ,w[N] ,ne[N] ,idx ;
bool vis[N] ;
int d[N] ;

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

void SPFA(){
    memset(d , 0x3f ,sizeof d) ;
    d[1] = 0 ;
    queue<int> q;
    q.push(1) ;
    vis[1] = true ;
    
    while(q.size()){
        auto t = q.front();
        q.pop();
        vis[t] = false ;
        
        for(int i = h[t] ; i != -1 ; i = ne[i] ){
            int jv = e[i] , jw = w[i];
            if(d[jv] > jw + d[t] ){
                d[jv] = jw + d[t] ;
                if(!vis[jv]){
                    q.push(jv) ;
                    vis[jv] = true ;
                } 
            }
        }
    }
}

int main(){
    memset(h ,-1 , sizeof h);
    int n,m ;
    cin >> n >> m ;
    for(int i = 1 ; i <= m ; i ++ ){
        int a , b , c ;
        cin >> a >> b >> c ;
        add(a , b , c);
    }
    SPFA() ;
    
    if (d[n] == 0x3f3f3f3f ) puts("impossible");
    else cout << d[n] << endl ;
    
    return 0 ;
}
判断负环

spfa算法判断负环利用了鸽巢原理。我们让cnt[i]代表从原点到i点所经历的负权边数量。每当有一条从i到j的边更新时,我们就可以理解为这一条是负权边,所以我们让cnt[j] = cnt[i] + 1 。当某个点从起点到终点的数量达到n时,代表到达这个点所经历了n条负权边。但图中一共只有n个点,所以必定存在一个环。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 100005 ;
int h[N] ,e[N] ,w[N] ,ne[N] ,idx ;
int d[N] ,cnt[N] ;
bool vis[N] ;

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


bool SPFA(int n){
    queue<int > q;
    for(int i = 1 ; i <= n ; i ++){
        q.push(i) ;
        vis[i] = true ;
    }
    
    while(q.size()){
        auto t = q.front();
        q.pop();
        
        vis[t] = false ;
        
        for(int i = h[t] ; i != -1 ; i = ne[i]){
            int jv = e[i] , jw = w[i] ;
            if(d[jv] > d[t] + jw){
                d[jv] = d[t] + jw ;
                cnt[jv] = cnt[t] + 1;
                if(cnt[jv] >= n) return true ;
                if(!vis[jv]) {
                    q.push(jv);
                    vis[jv] = true ;
                }
            }
        } 
    }
    return false ;
}

int main(){
    memset(h , -1 , sizeof h);
    int n , m ;
    cin >> n >> m ;
    
    for(int i = 1 ; i <= m ; i ++){
        int a , b , c ;
        cin >> a >> b >> c ;
        add(a ,b ,c );
    }
    
    bool tag = SPFA(n);
    
    if(tag) puts("Yes");
    else puts("No");
    
    
    return 0 ;
}

多源汇最短路

Floyd

此算法主要思想利用了动态规划,一句话概括就是,比较存起来的从i到j的距离和从i到k再从k到j的距离,然后取小者。

#include <iostream>
#include <cstring>
using namespace std ;
const int N = 205 ;

int d[N][N] ;

void floyd(int n){
    for(int k = 1 ; k <= n ; k ++)
        for(int i = 1 ; i <= n ; i ++ )
            for(int j = 1 ; j <= n ; j ++)
                d[i][j] = min (d[i][j] , d[i][k] + d[k][j]);
            
}

int main(){
    int n, m  ,q ;
    cin >> n >> m >> q ;
    memset(d , 0x3f , sizeof d) ;
    for(int i = 1 ; i <= n ; i ++){
        d[i][i] = 0 ;
    }
    
    while(m -- ){
        int a, b ,c;
        cin >> a >> b >> c ;
        d[a][b] = min (d[a][b] , c) ;
    }
    
    floyd(n) ;
    
    while( q -- ){
        int a ,b ;
        cin >> a >> b ;
        if ( d[a][b] > 0x3f3f3f3f / 2 ){
            puts("impossible");
        }
        else {
            cout << d[a][b] << endl ;
        }
    }
    
    return 0 ;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值