最短路问题

下面是我看acwing及一些博客之后乱七八糟的整理(主要参考yls)

是写给自己看的笔记 想要系统学习这一块的还是建议移步acwing

最短路问题存在着很多种情况 每一种情况都一定有它最好的选择 图里就归纳得很清楚了

这里值得注意的是 虽然是叫朴素Dijkstra算法 但是不是就比堆优化版的不好 只是分别适用于不同的情况而已

目录

图的存储

朴素Dijkstra算法

堆优化版的Dijkstra算法

bellman_ford算法

SPFA算法

Floyd算法


图的存储

稀疏图用邻接表 稠密图用邻接矩阵

边数默认为m  顶点数默认为n

当m~n^2时 属于稠密图

当m~n时 属于稀疏图


朴素Dijkstra算法

//是用自己的话总结的 到时候看看数据结构再改一下

(一个for循环里面包含着一个双重循环)

1.先做好初始化 dis[ ]数组及g[ ][ ] 都初始化为正无穷 源点距离为0

2.做n次迭代 每次都找到没有确定为最短路并且dis[ ]最小的点 t  再用 t 更新其他点的距离

代码模板:Dijkstra求最短路

#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
bool st[N];//标记有没有是否已经确定是最短路
int g[N][N];//因为题里的信息很明显是稠密图 所以用邻接矩阵
int dis[N];//记录各点到源点的距离
int m,n;
int dijkstra()
{
    memset(dis,0x3f,sizeof dis);//先把距离初始化为无穷大
    dis[1]=0;//起点距离自己的距离为0
    for(int i=0;i<n;++i)//迭代n次 每次确定一个点为最短路
    {
        int t=-1;//t存储当前路最短的点
        for(int j=1;j<=n;++j)//这个循环是遍历n个点 找到当前最短距离
        {
            if(!st[j]&&(t==-1||dis[j]<dis[t]))//如果发现更短的路径 就进行更新
                t=j;
        }
        st[t]=true;//该点最短距离确定
        for(int j=1;j<=n;++j)//用当前距离最小的点t更新其他点到起点的距离
            dis[j]=min(dis[j],dis[t]+g[t][j]);
    }
    if(dis[n]==0x3f3f3f3f)//如果到达不了n号节点
        return -1;
    else
        return dis[n];
}
int main()
{
    using namespace std;
    int a,b,c;
    memset(g,0x3f,sizeof g);
    cin>>n>>m;
    while(m--)
    {
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);//因为可能存在重边 所以要判断一下 取最小
    }
    cout<<dijkstra()<<endl;
}

注意
1.memset 按字节赋值 memset 0x3f 就等价与赋值为0x3f3f3f3f 所以memset用0x3f 在最后判断 dis[ ]的时候要判断是否==0x3f3f3f3f(一开始因为这里找了好久的错误)

2.这里是一个for循环里有两个for循环 注意花括号的位置 写的时候不要搞混了


堆优化版的Dijkstra算法

        由于朴素Dijkstra算法每次都要循环一遍 找dis[ ]最小的值 只是需要找最小的值 没有必要每次都遍历一遍dis数组 在一组数中很快地找到最小值 就能想到小根堆 可以用STL中的优先队列 也可以自己写一个堆 但比较麻烦

先学下优先队列

它的定义:priority_queue<Type, Container, Functional>

有些抽象了 不过目前我直接会用就行

升序队列:priority_queue <int,vector<int>,greater<int> > q;
降序队列:priority_queue <int,vector<int>,less<int> >q;

不过有些参数可以直接省略 比如STL默认第二个参数为vector 第三个默认为大根堆

priority_queue<int> q;//基础类型默认为大根堆 即降序队列

如果是pair 先比较第一个元素 第一个相等再比较第二个

它的其他函数与队列基本操作相同:

  • top 访问队头元素
  • empty 队列是否为空
  • size 返回队列内元素个数
  • push 插入元素到队尾 (并排序)
  • emplace 原地构造一个元素并插入队列
  • pop 弹出队头元素
  • swap 交换内容

        优先队列在这个算法里,存储的是各点编号及他们到源点的距离,因为这两个肯定是要捆绑在一起,所以要用到pair,又因为我们每次是要到源点距离最小的那个点 (pair优先比较第一个值),所以我们要把距离放在第一个

        值得注意的是 在这里每次更新值之后,每次更新的值都会被放入队列,原先的值不能删掉(自建堆能解决这个问题 不过自建堆有点麻烦)

        所以我们需要利用好st[ ]数组 在一开始的时候 如果st[ ]已经标记过这个点了 把该点出队之后就可以直接continue了

大概了解这些 来看看大概的思路

1.初始化g[ ]和dis[ ] 并把第一个点入队

2.如果队非空就取队头元素(因为是小根堆 所以该元素一定是当前路径最短的点)如果该点已经确认为最短路,就continue 看下一个 如果没有 st[ ]置为true 然后用该点去更新其他与它相邻的所有点 并把这些点也入队

3.最后如果dis[n]没有被更新过 仍然为正无穷 说明源点到不了n点 否则就返回dis[n]

模板题: Dijkstra求最短路 II 

代码:

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int N=1e6;
int n,m;
bool st[N];
int h[N],w[N],e[N],ne[N],idx;
int dis[N];

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra()
{
    priority_queue<PII,vector<PII>,greater<PII>>heap;//定义优先队列 小根堆
    memset(dis,0x3f,sizeof dis);//初始化所有点到源点的距离为正无穷
    dis[1]=0;//记得每次更新 既要修改数组dis 又要入队
    heap.push({0,1});//插入距离和顶点编号(这里不是结点编号)
    while(!heap.empty())
    {
        PII t=heap.top();//取队头元素(即最小值)
        heap.pop();
        int distance=t.first;//源点到ver的距离
        int ver=t.second;//顶点
        if(st[ver])//如果已经标记为最短路了
            continue;//下一个循环
        st[ver]=true;
        if(ver==n)//如果源点到n已经是最短距离 直接break就好了
            break;
        for(int i=h[ver];i!=-1;i=ne[i])//用当前最短距离的点ver更新所指向的结点距离
        {
            int j=e[i];
            dis[j]=min(dis[j],dis[ver]+w[i]);//每次修改都要进行两个操作
            heap.push({dis[j],j});
        }
    }
    if(dis[n]==0x3f3f3f3f)
        return -1;
    return dis[n];
}

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

bellman_ford算法

此算法适用于单源最短路中存在负权边并且限制要走k条路的情况

其实这个代码十分简洁

大概流程:(一个双重for循环)

1.先初始化dist数组为正无穷 然后做k次循环(因为限制了只能走k条路)

2.每次外层循环内都要遍历每一条边 更新值 在此之前要备份一下dis[ ]数组到backup数组[ ]里

有点说不清 先放下代码有边数限制的最短路

#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
const int M=10010;
int n,m,k;
int backup[M];//备份上一次的数据以防串联  
int dis[M];
struct Edge
{
    int a,b,w;
}edge[M];//把每条边都保存下来
int Bellman_ford()
{
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    for(int i=1;i<=k;++i)//最多走k条边
    {
        memcpy(backup,dis,sizeof dis);//备份上一次的数据
        for(int j=0;j<m;++j)
        {
            int a=edge[j].a;
            int b=edge[j].b;
            int w=edge[j].w;
            dis[b]=min(dis[b],backup[a]+w);//这个叫做松弛操作
            //避免刚更新完a又有用新的数据立马去更新b 这样就相当于走了两条边了
        }
    }
    if(dis[n]>0x3f3f3f3f/2)
        return -1;
    else
        return dis[n];
}

int main()
{
    cin>>n>>m>>k;
    int a,b,c;
    for(int i=0;i<m;++i)
    {
        cin>>a>>b>>c;
        edge[i]={a,b,c};
    }
    int ans=Bellman_ford();
    if(ans==-1)
        puts("impossible");
    else
        cout<<ans<<endl;
}

Q1:什么是串联?

A1:就是在一个外层for循环内 更新一个距离之后又用这个刚刚更新的值再去更新一个点 这样就相当于走了两条边

Q2:backup[ ]数组的作用?

A2:为了防止串联的发生 这样就可以用原本的上一次的数据去更新了

Q3:为什么子函数里最后要判断dis[n]>0x3f3f3f3f/2 不是直接等于0x3f3f3f3f?

A3:因为0x3f3f3f3f并不是真的无穷大 可能存在负权边的情况 更新值的时候有可能得到比如10^9-10这样的情况 但是其实它在k条路之内并没有和源点连通

Q4:关于边的存储

A4:用y总的话来说 “这个算法就比较牛逼了 随便存 随便存 只要每次循环都能遍历到所有边就可以了“


SPFA算法

        上面的Bellman_ford算法每次都会遍历所有的边 但是很多边遍历了其实没什么意义 只有当一个点的前驱结点更新了 该节点才会得到更新 所以我们可以创一个队列存储每一次距离被更新的结点 出队的时候就更新该结点的所有后驱结点

        这个算法与Dijkstra算法很像 但是还是注意区分开 这里的st[ ]数组标记的是该点有没有在队列中 在这里的意思是该点有没有被更新过 它的变换是可逆的 但是dijkstra算法中st[ ]是不可逆的 st标为true之后就不能再更改 它已经确认为最短路了

        用spfa算法在大多数情况下其实也可以解决dijkstra算法解决的问题并且时间会比dijkstra算法快很多 但是有些出题人可能会故意卡spfa spfa的最坏情况的时间复杂度高达O(nm)

代码: spfa求最短路

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
bool st[N];
int dis[N];

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

int spfa()
{
    memset(dis,0x3f,sizeof dis);
    queue<int>q;
    dis[1]=0;
    q.push(1);
    st[1]=true;
    while(!q.empty())
    {
        int t=q.front();
        q.pop();
        st[t]=false;//从队列中取出来之后该节点st被标记为false 之后该节点如果发生更新可再次入队
        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(!st[j])//当前已经入队的结点无需再次加入队 即便发生了更新也只用更新数值即可 重复添加降低效率且没有必要
                {
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    if(dis[n]==0x3f3f3f3f)
        return -1;
    else
        return dis[n];
}
int main()
{
    int a,b,c;
    memset(h,-1,sizeof h);
    cin>>n>>m;
    while(m--)
    {
        cin>>a>>b>>c;
        add(a,b,c);
    }
    int t=spfa();
    if(t==-1)
        puts("impossible");
    else
        printf("%d\n",t);
}

这个算法也能判断是否存在负权回路(Bellman_ford算法也可以 但是效率太低 一般不用它)

我们可以开一个cnt[ ]数组记录最短路经过的边的条数 如果经过边数大于等于点数n 根据抽屉原理 说明一定存在负权回路

代码:spfa判断负环 

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
bool st[N];
int dis[N];
int cnt[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)//因为负权回路不一定是从源点出发会经过的路 所以需要每个点都要走一遍
        q.push(i);
    while(!q.empty())
    {
        int t=q.front();
        q.pop();
        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];
                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n)
                    return true;
                q.push(j);
            }
        }
    }
    return false;
}
int main()
{
    int a,b,c;
    memset(h,-1,sizeof h);
    cin>>n>>m;
    while(m--)
    {
        cin>>a>>b>>c;
        add(a,b,c);
    }
    if(spfa())
        puts("Yes");
    else
        puts("No");
    return 0;
}

Q1:为什么不用初始化dis[ ]?

A1:如果存在负环,不管dis[ ]初始化为多少,都会被不断更新,所以dis初值对结果没有影响

Q2:为什么去掉了st[ ]的判断

A2:因为判负环的原理是看cnt数组是否大于n,cnt更新的前提是当前点更新,而队列的存在让我不能够很快的访问同一个点,当去掉判断以后,很快我就能重复遇到相邻的点,这样能够快速增加cnt的值,所以会快很多。(——Aku)


Floyd算法

其实不是太能理解核心代码 或者说是不太理解动态规划 但是好在比较好记

大概步骤

1.初始化d[ ]数组

2.三重循环k i j 更新d[ ]

代码:Floyd求最短路

#include<iostream>
#include<cstring>
using namespace std;
const int N=210;
const int inf=0x3f3f3f3f;
int d[N][N];
int m,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()
{
    int k;
    cin>>n>>m>>k;
    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,c;
        cin>>a>>b>>c;
        d[a][b]=min(d[a][b],c);//因为存在重边
    }
    Floyd();
    while(k--)
    {
        int a,b;
        cin>>a>>b;
        if(d[a][b]>inf/2)//因为存在负权边 所以这里要大于inf/2
            puts("impossible");
        else
            cout<<d[a][b]<<endl;
    }
}

至此

终于学完最短路所有模板题

没有学完 只能说看完 继续努力

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值