【无标题】

最短路算法

单源最短路

  1. 所有边权都正数
  • Dijstra
    • 时间复杂度:O(n^2)
      • 实现方法:
        • 从起点开始每次从未更新过的点中找到距离起点的距离最近的点
        • 用这个点更新起点到其余所有点的最短距离
        • 更新结束将这个点放入集合,保证集合中装的全部是已经完成最短距离统计的点
        • 重复上述操作,每次进入集合一个点,当n次全部结束之后,第n个点进入集合,那么dis[1~n]即为起点到各个点的最短距离
      • 代码实现
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

int g[505][505];//稠密图使用邻接矩阵存图,方便初始化为无穷的操作
int mark[505];//更新过的点即走过的点集合
int dis[505];//1点到各个点的最小距离
int n , m , a , b , c;


int dij(){
    //先将1到所有点的距离全部初始化为无穷大;
    memset(dis , 0x3f , sizeof dis);
    
    //将初始点距离设置为0
    dis[1] = 0;
    
    //开始更新n次,知道更新到n这个点
    for(int i = 1 ;i <= n ; i++){
        
        //用来找出剩余所有未走过的点中dis距离最小的点
        int t = -1;
        
        //扫描1-n所有点找出此时未走过的点dis最小值
        for(int j = 1 ; j <= n ; j++){
            
            //如果这个点没走过,并且是所有没走过的点中dis最小的,t一直在帮助记录最小值的点,出现第j个点比上一个统计到了最小值点t
            //还要小,那么就更新t
            if(!mark[j] && (t == -1 || dis[t] > dis[j]) ){
                t = j;
            }
        }
        
        //找到最小点之后,要将最小点标记为走过
        mark[t] = 1;
        
        //然后用这个点将他能到达的所有点进行松弛,也就更新这个点到各个点的dis,因为从这个点出发到达到达其他点,可能会因为路径不同
        //而产生dis的变得更小
        for(int j = 1 ; j <= n ; j++)
        
            //函数调用时先将dis全部设置为无穷大就是因为很多无法从t到达的点,会因为初始值为无穷大,所以加上t的dis,还是无穷大
            //不会对我们更新距离产生影响
            dis[j] = min(dis[j] , dis[t] + g[t][j]);//用原来从其他点到j点的最短距离和从t点到j点的最短距离做最小值比较,更新dis[j]
    }
    
    //这里为啥不是等于而是大于。因为如果到达不了n点
    //那么n点会因为每次的当j=n时的dis[j] = min(dis[j] , dis[t] + g[t][j]);不停的增大
    if(dis[n] >= 0x3f3f3f3f) return -1;
    return dis[n];
}
int main(){
    cin >> n >> m;
    memset(g , 0x3f , sizeof g);//将边数组初始化
    for(int i = 1 ; i <= m ; i++){//存图
        scanf("%d %d %d" , &a , &b , &c);
        if(g[a][b] > c) g[a][b] = c;
    }
    cout << dij();
    return 0;
}
  • 堆优化版Dijstra
    • 时间复杂度:O(mlogn)
    • 实现方法和朴素Dij做法相同,只不过利用堆的特性来优化查找最小值的点的过程
    • 代码实现
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn = 2e5 + 10;

//first为1点到第second点最短距离 , second内装的是第几个点
typedef pair<int , int> PII;

//使用优先队列来构造大根堆,将每次查找最小值的过程优化为logn
priority_queue<PII , vector<PII> , greater<PII> > q;

//已确定最短距离的集合
int mark[maxn];

//1-n所有点的距离
int dis[maxn];

//链式前向星存边以及边权
int h[maxn] , e[maxn] , w[maxn] , ne[maxn] , idx;

int n , m;

//链式前向星加边
void add(int a , int b , int c){
    e[idx] = b , w[idx] = c , ne[idx] = h[a] , h[a] = idx++;
}

//dij实现函数
int ddij(){
    //将1到1~n所有点的距离初始化为无穷大,待更新
    memset(dis , 0x3f , sizeof dis);
    
    //从1开始将1到1本身的距离赋值为0
    dis[1] = 0;
    //将第一个初始点进队,开始更新
    q.push({0 , 1});
    
    //开始更新
    //如果队列没有为空,证明还有能到达的点未被更新
    while(!q.empty()){
        
        //将此时所有能到达的待更新的点中最小的dis拿出来
        PII t = q.top();
        q.pop();
        int v = t.second;//v存储从哪个点开始

        //判断过滤重边的问题
        if(mark[v]) continue;
        
        //更新完出队,即进入集合
        mark[v] = 1;

        //开始用此时最小点v更新v所能到达的点的最小距离
        for(int i = h[v] ; i != -1 ; i = ne[i]){
            int k = e[i];//存储v的出边中第i条边到达哪个点
            if(dis[k] > dis[v] + w[i]){//如果从v这个点出发到达k点的距离小于之前的更新的k的最小距离
                dis[k] = dis[v] + w[i];//那么就更新起始点1到k的最小距离dis[k]
                q.push({dis[k] , k});//将k这个点以及他此时的最小距离入队
            }
        }
        //将所有v能到达的点的距离全部更新结束之后,再去队头找到此时dis最小的点,再重复
    }
    //直到全部完成出队即完成全部点的更新
    if(dis[n] == 0x3f3f3f3f) return -1;
    return dis[n];
    
}
int main(){
    cin >> n >> m;
    //初始化链式前向星全部头结点数组为-1,表示目前从此结点还没有任何一条出边
    memset(h , -1 , sizeof h);
    for(int i = 1 ; i <= m ; i++){
        int a , b , c;
        scanf("%d %d %d" , &a , &b , &c);
        add(a , b , c);
    }
    cout << ddij();
    return 0;
}
  1. 存在负权边
  • Bellman Ford
    • 时间复杂度:O(nm)
    • 实现方法:
      • 从起点开始,逐层更新
      • 每层即为起点到n的边数增1
      • 一定要注意负权回路的问题,如果不规定遍历层数即边数,那么会进入无线回环,不存在最短路径
      • bellmanford算法可以用来判断是否有环,即n个点n条边一定是因为一个点充当两个点导致的
      • 代码实现
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 1e5 + 10;

//用结构体存图
struct node{
    int u , v , w;    
}arr[maxn];

//定义两个数组,dis存储距离,backdis是用来更新所有边时记录上一层的全部状态防止发生串联更新的
int dis[505] , backdis[505];
int n , m , k;

//函数实现
bool bellman(){
    //将全部dis初始化为无穷大,待更新
    memset(dis , 0x3f , sizeof dis);
    //将起始点距离初始为0
    dis[1] = 0;
    //遍历k层,即限制k条边到达n点
    for(int i = 1 ; i <= k ; i ++){
        //将本轮用来更新所有边的这一层状态记录下来
        memcpy(backdis , dis , sizeof dis);
        //开始更新所有边
        for(int j = 1 ; j <= m ;j++){
            //记录更新的第j条边的起点,终点,边权
            int u = arr[j].u , v = arr[j].v , w = arr[j].w;
            //做一个松弛操作
            dis[v] = min(dis[v] , backdis[u] + w);
        }
    }
    //因为随着每轮的松弛,因为存在负权边,所以随着轮数增多,如果出现负权边,那么会导致dis[n]的值,会被降低,但是因为
    //题目数据的大小(边数为10000),所以不会大的变化
    if(dis[n] >= 0x3f3f3f3f / 2) return true;
    return false;
}
int main(){
    cin >> n >> m >> k;
    for(int i = 1 ; i <= m ; i++){
        int a , b , c;
        scanf("%d %d %d" , &a , &b , &c);
        arr[i] = {a , b , c};
    }
    if(bellman()) cout << "impossible";
    else cout << dis[n];
    return 0;
}
  • SPFA
    • 时间复杂度:O(mn)
    • 实现方法:SPFA其实就是对于bellmanford的优化,因为bellmannford算法中遍历每轮的点并不一定会涉及到每一条边,因此,不需要真的遍历每一天做松弛,并且我们发现对于dis[j] > min(dis[j] , dis[t] + w[i])这个松弛操作来说,只有dis[t]发生了减小,dis[j]才会出现更新,因此只要使用队列来维护一个只包含被更新过距离的点组成的集合,然后依次对队列中被更新过距离点做操作,将这个点能到达的点进行松弛即可
    • 代码实现
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;

const int maxn = 1e5 + 10;

queue<int > q;
int h[maxn] , w[maxn] , e[maxn] , ne[maxn] , idx; 
int mark[maxn];
int dis[maxn];
int n , m;

//链式前向星存图
void add(int a , int b , int c){
    e[idx] = b , w[idx] = c , ne[idx] = h[a] , h[a] = idx++;
}

//spfa实现函数
int spfa(){
    //初始化距离数组
    memset(dis , 0x3f , sizeof dis);
    dis[1] = 0;
    
    //起始点入队
    q.push(1);
    //入队的点打好标记表示此点的距离被更新到更小,因此要将其能到达的点全部做一次更新
    mark[1] = 1;
    //只要队列不为空,证明还有被更新过的点没有将它能到达的点进行延展
    while(!q.empty()){
        int t = q.front();
        //延展完出队清除记录
        q.pop();
        mark[t] = 0;//mark里保存的值是被更新变小的值,而不是走过的点,如果某个点先进了队列,然后以他为头,将他能到达的
        //点更新了一遍,他出队,但是由于存在负权边,所以如果之后他被其他点又重新延展了一次的话,他本身的点如果降低了,
        //那么必须再进队重新去延展他能到达的点,这就是为啥出队要清零
        
        //遍历t点能到达点,对于松弛一遍全部距离,出现距离被更新,那么查看是否在队列中,如果在队列中,不需要再次进队
        //如果不在队列那么要进队,因为只有在队列中的点才会将其能到达的点更新,所以既然发生了降低,那么一定会对其能到达
        //的点产生影响
        for(int i = h[t] ; i != -1 ; i = ne[i]){
            int j = e[i];
            if(dis[j] > dis[t] + w[i]){
                dis[j] = dis[t] + w[i];
                if(!mark[j]){
                    q.push(j);
                    mark[j] = 1;
                }
            }
        }
    }
    if(dis[n] == 0x3f3f3f3f) return 0;
    return 1;
}
int main(){
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for(int i = 1 ; i <= m ; i++){
        int a , b , c;
        scanf("%d %d %d" , &a , &b , &c);
        add(a , b , c);
    }
    if(spfa()) cout << dis[n];
    else cout << "impossible";
    return 0;
}

多源汇最短路

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值