对于最短路问题的复习与总结

首先告诉大家什么时候用邻接表去存,什么时候用邻接矩阵去存:

根据邻接表和邻接矩阵的结构特性可知,当图为稀疏图、顶点较多,即图结构比较大时,更适宜选择邻接表作为存储结构。

判断稀疏和稠密的因素是判断E(点)和pow(V,2)(边的平方)的大小,如果E远小于pow为稀疏图;如果差不多数量为稠密图

如何选择方法:

一:dijkstra算法求最短路(朴素版)

dijkstra朴素版使用邻接矩阵的存储形式去存储数据,矩阵矩阵,顾名思义就是使用二维数组去a[x][y]去表示x点到y点的距离。

接下来上题目

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式

第一行包含整数 n 和 m。

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

输出格式

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

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

数据范围

1≤n≤500
1≤m≤10e5,
图中涉及边长均不超过10000。

输入样例:

3 3
1 2 2
2 3 1
1 3 4

输出样例:

3

接下来我们上代码,在相关代码上我都有注释帮助大家去理解

#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int N=550;
int g[N][N]; //邻接矩阵去存储
int dist[N]; //表示第x(下标)个点到1号点的距离
int n,m;
int a,b,w;  //a点到b点边的权重为w
bool vis[N]; //判断数组,判断当前这个边是不是已经被更新

int dijkstra(){
    
    memset(dist,0x3f,sizeof(dist));  //首先我们初始化所有点到1号点的距离为正无穷
    dist[1]=0;  
    for(int i=1;i<n;i++){  //进行n次或n-1次循环
        int t=-1;  //这个点第一次进去,用t=-1去标记它
        for(int j=1;j<=n;j++)  
        if(!vis[j]&&(t==-1||dist[t]>dist[j]))  
        t=j;   //这一重循环是为了找到距离1号点最近的点t
        
        //cout<<"3331"<<' '<<t<<' '<<dist[t]<<' ';
        vis[t]=true;  //t这个点已经更新过了,做标记
        
        for(int j=1;j<=n;j++){
            dist[j]=min(dist[j],dist[t]+g[t][j]); //用t这个点去更新其他点的距离
        }
       // cout<<dist[n]<<endl;
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}

int main(){
    cin>>n>>m;
    memset(g,0x3f,sizeof(g));
   while(m--){
        cin>>a>>b>>w;
        g[a][b]=min(g[a][b],w);
    }
   int t=dijkstra();
       printf("%d",t);
}

注意:dijkstra不能处理负边问题,下面有一篇文章讲的挺清楚的,大家可以去看看

https://blog.csdn.net/sugarbliss/article/details/86498323

 二:dijkstra算法求最短路(堆优化版)

dijkstra优化版使用邻接表的形式去存储数据,那么我们首先介绍一下邻接表的定义和代码实现

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

这段代码的含义就是将所有与a相连的边存储形成一个链表,例如  看一个题目的样例

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出 −1−1。

输入格式

第一行包含整数 n 和 m。

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

输出格式

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

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

数据范围

1≤n,m≤1.5×105,
图中涉及边长均不小于 00,且不超过 10000
数据保证:如果最短路存在,则最短路的长度不超过 10e9。

输入样例:

3 3
1 2 2
2 3 1
1 3 4

输出样例:

3

那么题目上的三条边是 1 2 2,2 3 1,1 3 4,在进行完我们的加边操作之后,存储的形式就会变成

1-2-3,2-3,就会变成这种形式 

 明白这个建表方式之后,我们来看看题目该如何去处理

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int,int> PII;
const int N=200000;
int h[N],e[N],w[N],ne[N],idx;
bool vis[N];
int dist[N];
int n,m;
int a,b,c;

void add(int x,int y,int z){
    e[idx]=y;
    w[idx]=z;
    ne[idx]=h[x];
    h[x]=idx++;
}

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    priority_queue<PII,vector<PII>,greater<PII> >heap; //用小根堆去存数据,它会将较小的数据放在堆顶
    heap.push({0,1});  //1号点距离1号点的距离是0,{first(距离),second(当前点)}
    while(heap.size()){
        auto t=heap.top();  //一定是当前队列里距离1号点最近的点;
        heap.pop();   
        int distance=t.first;
        int ver=t.second;
        if(vis[ver]) continue;    //如果已经是最短的了,就进行下一次循环
        vis[ver]=true;   //不是的话就标记
        for(int i=h[ver];i!=-1;i=ne[i]){  //用当前这个距离1号点最近的点去更新与当前这个点(x)相连的所有边(也就是x做链表头的这个链表)
            int j=e[i];   //e[i]是这个点b
            if(dist[j]>dist[ver]+w[i])
            {
                dist[j]=dist[ver]+w[i];
                heap.push({dist[j],j});
            }
        }
        
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
        
    
}

int main(){
    memset(h,-1,sizeof(h));
    cin>>n>>m;
    while(m--){
        cin>>a>>b>>c;
        add(a,b,c);
    }
    cout<<dijkstra();
}

 三:bellman_ford算法求最短路

bellman_ford算法适用于一些有边数限制的最短路问题,我们想一想刚才我们解决的两个问题,他们都是要求1号点到n号点的最短路径是多少,题目并没有给我们一个限制走多少条边,那现在我要求你在5条边之内到达n点,并且要求距离最短,那么你该如何去解决这个问题呢?

接下来我们就来看一道这样的题目:

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出从 1号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible

注意:图中可能 存在负权回路 。

输入格式

第一行包含三个整数 n,m,k。

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

点的编号为 1∼n。

输出格式

输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible

数据范围

1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。

输入样例:

3 3 1
1 2 1
2 3 1
1 3 3

输出样例:

3

大家先看代码,注意代码中的last数组,想一想为什么要多用一个last的数组,而不是直接用dist数组,同时边权可以为负数

#include<iostream>
#include<cstring>
using namespace std;
const int N=510,M=10010;
int n,m,k;
int dist[N],last[N];
int a,b,c;

typedef struct Edge{
    int a;
    int b;
    int w;
}; //用结构体来存边
Edge edges[M];

void bellman_ford(){
      dist[1]=0;
      for(int i=0;i<k;i++){   //因为要在k条边内解决掉问题,所以我们只需要进行k次循环
        memcpy(last,dist,sizeof(dist));  //记录上次迭代的数据
        for(int j=0;j<m;j++){
            auto e=edges[j];   //更新距离这m条边的距离
            dist[e.b]=min(dist[e.b],last[e.a]+e.w);  
        }
        
    }
}

int main(){
    memset(dist,0x3f,sizeof(dist));
    cin>>n>>m>>k;
    for(int i=0;i<m;i++){
        cin>>a>>b>>c;
        edges[i]={a,b,c};
    }
  bellman_ford();
  if(dist[n]>0x3f3f3f3f/2) cout<<"impossible";
  else cout<<dist[n];
    
}

注意last数组的用途!!!!

看样例,如果把这个last换成dist的话,在更新3号点的时候2号点已经被更新了,那么3号点的更新会在2的基础上更新,更新过后dist[3]=2,但是这个时候用了2条边,超过了k的值,这就是为什么我们要用上一次的迭代结果去执行这次的更新。

不知道大家有没有一个疑问,bellman_ford算法的代码量怎么少,为什么不用他去解决dijkstra的问题呢,只需要把第一层for循环改为n层,起初我也有这样的疑问,所以我也就去尝试了,但是结果是Segmentation Fault ,也就是我们常说的爆内存,在看了这两道题的数据范围后,我认为(仅仅是我认为),

在dijkstra中边是非常多的,m<100000;

在bellman_ford中边是少的,1≤m≤10000,这可能也是他为什么使用结构体来存储边的原因了,所以在边数过多时爆了。

这个问题我还没想明白,如果有大佬明白的话,可以私信帮帮我

四.spfa算法求最短路

题目

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出 11 号点到 nn 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible

数据保证不存在负权回路。

输入格式

第一行包含整数 n 和 m。

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

输出格式

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

如果路径不存在,则输出 impossible

数据范围

1≤n,m≤10e5
图中涉及边长绝对值均不超过 10000。

输入样例:

3 3
1 2 5
2 3 -3
1 3 4

输出样例:

2

在理解了上面的几个问题之后,在接着往下看,难度就慢慢减小了。先看点和边,发现范围差不多,所以我们选择邻接表来存

上代码

#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],w[N],e[N],ne[N],idx;
int dist[N];
bool st[N];
int a,b,c;
//用更新过的点去更新别的点,因为第一次更新进去队列的是距离小的,所以用他去更新其他的点
void add(int a,int b,int c){
    e[idx]=b;
    w[idx]=c;
    ne[idx]=h[a];
    h[a]=idx++;
}

int spfa(){
    dist[1]=0;
    queue<int>q;
    q.push(1);
    st[1]=true;
    while(q.size()){
        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;
                }
            }
        }
    }
    return dist[n];
}


int main(){
    memset(dist,0x3f,sizeof(dist));
    memset(h,-1,sizeof(h));
    cin>>n>>m;
    while(m--){
        cin>>a>>b>>c;
        add(a,b,c);
    }
   // spfa();
    int t=spfa();
    /*for(int i=1;i<=m;i++){
        if(st[i]==true)
        cout<<i;
    }*/
    if(t==0x3f3f3f3f) cout<<"impossible";
    else cout<<t;
}


spfa算法的牛逼之处就在于他每次更新都是用的对头元素去更新,而我们发现队列里的元素都是被更新过的,也就是说用更新过的点去更新其他不在队列里的点

看文字不能清楚理解

建议大家去看看这个大佬的图片模拟,看完图之后在回来看代码,会豁然开朗的

https://www.acwing.com/solution/content/105508/

spfa和dijkstra的区别:
st用来检验队列中是否有重复的点
spfa从队列中使用了当前的点,会把该点pop掉,状态数组st[i] = false(说明堆中不存在了) ,更新临边之后,把临边放入队列中, 并且设置状态数组为true,表示放入队列中 。如果当前的点距离变小,可能会再次进入队列,因此可以检验负环:

每次更新可以记录一次,如果记录的次数 > n,代表存在负环(注意这句话,这也就是接下来我们spfa判断是否有负环的理论基础)(环一定是负的,因为只有负环才会不断循环下去)。

五:spfa求负环

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你判断图中是否存在负权回路。

输入格式

第一行包含整数 n 和 m。

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

输出格式

如果图中存在负权回路,则输出 Yes,否则输出 No

数据范围

1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。

输入样例:

3 3
1 2 -1
2 3 4
3 1 -4

输出样例:

Yes

这个思路就是我们上一题所说的,就直接上代码了

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


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

bool spfa(){
    queue<int>q;
    for(int i=1;i<=n;i++){
        st[i]=true;
        q.push(i);
        
    }
    
    while(q.size()){
        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 true;
               
                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
        
            }
        }
    }
    return false;
}

    int main(){
        scanf("%d%d",&n,&m);
        memset(h,-1,sizeof(h));
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            add(a,b,c);
        }
        if(spfa())  puts("Yes");
        else puts("No");
        return 0;
    }


六:Floyd求最短路

这种方法适用于多源最短路,但是证明和简化代码需要用dp来做支撑,但我还没有掌握dp大法的简化,反而代码不长不难,记起来倒是方便。

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible

数据保证图中不存在负权回路。

输入格式

第一行包含三个整数 n,m,k。

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

接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。

输出格式

共 kk 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible

数据范围

1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。

输入样例:

3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3

输出样例:

impossible
1

 代码实现

#include<iostream>
#include<algorithm>
using namespace std;
const int N=210,INF=1e9;
int n,m,Q;
int d[N][N];

void floyd(){
    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(){
    scanf("%d%d%d",&n,&m,&Q);
    for(int i=1;i<=n;i++)
     for(int j=1;j<=n;j++)
      if(i==j) 
      d[i][j]=0;
      else
      d[i][j]=INF;
    while(m--){
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        d[a][b]=min(d[a][b],w);
    }
    floyd();
    while(Q--){
        int a,b;
        scanf("%d%d",&a,&b);
        if(d[a][b]>INF/2) puts("impossible");
        else printf("%d\n",d[a][b]);
    }
    
}

 

代码量并不多,大家可以背下来

最短路的问题就到这了,这是好久以前学的知识了,最近回过头来一看,搞不明白了,就写了这篇博客帮助自己复习一下,一定要多复习,不然回过头来啥都忘了。能力还很有限,如果有不清楚或错误的地方欢迎大家指出,我继续百度学习  手动狗头狗头

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值