图的最短路问题(spfa-Dijkstra-floyd-模板代码c++)

文章首发于:My Blog 欢迎大佬们前来逛逛

图论:三种最短路及模板

注意:在这三种算法 中我均使用的 链式前向星存图,具体方式请看我这篇博客:链式前向星存图详解

模板

SPFA算法

spfa是优化后的Bellmax-ford算法,采用了 队列进行优化

SPFA可以用来处理负权边的情况,通常用于求带负边权的单源最短路径问题

  1. 首先进行链式前向星存图
  2. 然后对于起点s到各个点i的最短距离 dis[i] 我们初始化为一个极大值,以便之后可以更新到最短路径
  3. 把起点放入队列中,然后取出队头,用u来记录此时需要松弛的点(就是队头元素),遍历起点u能够到达的所有的终点v,进行u到v的最短距离的更新,如果某一个v点没有被松弛过,则把这个v点放入队列中,等待之后松弛v点,更新最短距离。
  4. 直到所有的点都被松弛过了,则这些点都是True状态,队列中元素为空,退出循环。
  5. 因此最后dis存储的就是s到所有点的最短路径
//TODO: Write code here
int n,m,s;
const int N=1e5+10;
int nums[N];
struct Edge
{
    int to,w,next;
}edge[N];
int dis[N];
bool vis[N];
int head[N],cnt;
void add_edge(int u,int v,int w)
{
    edge[++cnt].next=head[u];
    edge[cnt].to=v;
    edge[cnt].w=w;
    head[u]=cnt;
}
void spfa()
{
    for (int i=1;i<=n;i++)
    {
        dis[i]=INF;   //起点到i点的距离为一个极大值
        vis[i]=false;
    }
    queue<int> q;
    //起点入队,标记起点,起点距离为0
    q.push(s);
    vis[s]=true;
    dis[s]=0;
    while (!q.empty())
    {
        auto p=q.front();   //取出这个待优化的点
        q.pop();
        vis[p]=false;
        for (int i=head[p];i;i=edge[i].next)
        {//寻找与p队头元素相连的边
            int v=edge[i].to;
            if (dis[v]>dis[p]+edge[i].w)//对 p->v 的每条边寻找一个最短路径的边
            {
                //松弛操作
                dis[v]=dis[p]+edge[i].w;
                if (!vis[v])
                {
                    q.push(v);
                    vis[v]=true;
                }
            }
        }
    }
}
signed main()
{
	cin>>n>>m>>s;
    for (int i=1;i<=m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        add_edge(u,v,w);
    }  
    spfa();
    for (int i=1;i<=n;i++)
    {
        cout<<dis[i]<<" ";
    }
#define one 1
	return 0;
}

Floyd算法

弗洛伊德算法使用三重循环,时间复杂度较高

floyd算法详解

floyd算法的核心:从i点到j点经过k个点的情况下所能经过的最短距离。

floyd算法可以求得任意两点之间的最短路径

//TODO: Write code here
int n,m,s;
const int N=1e4+10;
int dis[N];
int G[N][N];
signed main()
{
	cin>>n>>m>>s;
    for (int i=1;i<=n;i++)
    {
        for (int j=1;j<=n;j++)
        {
            G[i][j]=INF; //初始化i到j点的最短路径为一个极大值
        }
    }
    for (int i=1;i<=m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        G[u][v]=min(G[u][v],w);  //u到v点的距离存储,要注意输入可能会重复
    }
    for (int k=1;k<=n;k++)
    {
        for (int i=1;i<=n;i++)
        {
            for (int j=1;j<=n;j++)
            {
                G[i][j]=min(G[i][j],G[i][k]+G[k][j]); //三重循环求i到j的最短距离
            }
        }
    }
    G[s][s]=0;
    for (int i=1;i<=n;i++)
    {
        cout<<G[s][i]<<" ";
    }
#define one 1
	return 0;
}

Dijkstra算法

迪杰斯特拉算法采用堆优化的方法,是求最短路三种方法中的较优的方法

获取每一个点的顶点编号,然后对这个顶点相连的所有边进行遍历求最短路径,并且处理完成之后把每个点入队,然后接下来继续对队列中的点弹出并且继续求最短路径,直到队列为空。

另外,如果我们的起点到 一个点有多条路径,则dis会记录当前一条路径的最短路径,而 pair的first记录的则不一定是最短路径,则如果 我们发现 dis[u] < p.first 则直接跳转下一个点即可,表示的是当前到该点的路径已经是最短的了,无需再次进行松弛操作。

//TODO: Write code here
int n,m,s;
const int N=1e6+10;
struct Edge
{
    int to,val,next;
}edge[N];
int head[N],cnt,dis[N];
void add_edge(int u,int v,int w)
{
    edge[++cnt].next=head[u];
    edge[cnt].to=v;
    edge[cnt].val=w;
    head[u]=cnt;
}
void dijsktra()
{   
    for (int i=1;i<=n;i++)
    {
        dis[i]=INF;
    }
    priority_queue<PI,v<PI>,greater<PI>> q;
    dis[s]=0;
    q.push({0,s});  //起点入堆
    while (!q.empty())
    {
        PI p=q.top();
        q.pop();
        int u=p.second;//获取以u为起点的顶点编号
        if (dis[u]<p.first) continue;
        for (int i=head[u];i;i=edge[i].next)//遍历与u相连的所有点,寻找最短路径
        {
            int v=edge[i].to;
            if (dis[v]>dis[u]+edge[i].val)
            {
                dis[v]=dis[u]+edge[i].val;
                q.push({dis[v],v});
            }
        }
    }
}
signed main()
{
	cin>>n>>m>>s;
    for (int i=1;i<=m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        add_edge(u,v,w);    //单向边
    }
    dijsktra();
    for (int i=1;i<=n;i++)
    {
        cout<<dis[i]<<' ';
    }
#define one 1
	return 0;
}

例题与应用

反向建边

例题:邮递员送信

有一个邮递员要送东西,邮局在节点 1。他总共要送 n−1 样东西,其目的地分别是节点 2 到节点 n。由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 m条道路。这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。求送完这n−1 样东西并且最终回到邮局最少需要的时间。


分两步求得结果:正向送出和反向回来的最短时间

  • 正向送出:直接使用一次 dijktra算法计算单源最短路径即可

但是我们发现还需要求从每个点再回到起点的最短路径。

我们能否求出起点到终点的距离乘2 ??这不就是往返两趟的路程吗???

请注意: 道路是单向边,过去有道路可走,回来不一定有路!

因此,本题核心:

  • 反向回来:反向建边,再进行一次反向的 disktra

起点到每个点:一对多的关系;每个点到起点:多对一的关系,我们要把它转换为一对多的关系


如何反向建边??

  1. 使用邻接矩阵:邻接矩阵存边,是采用数组的形式,记录的 u 到 v 的边用 edge[u] [v]表示,正向的时候,我直接用即可;反向的时候:我们采用 swap,交换每一个 u 与 v ,这就成了反向的图了

  2. 使用链式前向星存图: add_edge添加 u 到 v,权值为 w的边,则我们把 u 加上 n,v 加上 n,确保点之间不会出现冲突,然后 add_edge添加 v+n 到 u+n,权值为 w 的边,这样既不会产生冲突,我们也建了n条反向的边

然后分别进行 dijkstra算法即可,我们传入每个的起点,分别为 1 和 1+n,同时注意dis 的清除

//TODO: Write code here
int n,m;
const int N=5e6+10;
int nums[N];
struct Edge
{
    int to,w,next;
}edge[N];
int head[N],cnt,dis[N];
void add_edge(int u,int v,int w)
{
    edge[++cnt].next=head[u];
    edge[cnt].to=v;
    edge[cnt].w=w;
    head[u]=cnt;
}
void dijktra(int s)
{
    memset(dis,0x3F,sizeof(dis));
    dis[s]=0;
    priority_queue<PI,v<PI>,greater<PI>> q;
    q.push({0,s});
    while (!q.empty())
    {
        auto p=q.top();
        q.pop();
        int u=p.second;
        if (dis[u]!=p.first) continue;
        for (int i=head[u];i;i=edge[i].next)
        {
            int v=edge[i].to;
            if (dis[v]>dis[u]+edge[i].w)
            {
                dis[v]=dis[u]+edge[i].w;
                q.push({dis[v],v});
            }
        }
    }
}
signed main()
{
	cin>>n>>m;
    for (int i=1;i<=m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        add_edge(u,v,w);
        add_edge(v+n,u+n,w); //建反向边
    }
    int ans=0;
    dijktra(1); //正向的送出
    for (int i=2;i<=n;i++)
    {
        ans+=dis[i];    
    }
    dijktra(1+n); //反向的回来
    for (int i=2+n;i<=(n<<1);i++)
    {
        ans+=dis[i];    
    }
    cout<<ans;
#define one 1
	return 0;
}

最短路计数

题目传送门

给出一个 N 个顶点 M 条边的无向无权图,顶点编号为 1∼N。问从顶点 1 开始,到其他每个点的最短路有几条。

题目让我们对最短路径条数进行计数

并且这个图是一个无向无权图,因此我们假设每一条边的权值为 1,进行正常的求最短路径的过程

只不过我们需要一个ans数组,ans[i] 表示从 1 到 i 点的最短路径的条数

我们在利用dijsktra求出 从 u到 v 点的路径长度的时候,需要初始化 ans[v] 为 ans[u]条路径 ;当有多条路径的时候,则此条路径的最短路径长度应该和我们之前记录的最短的长度是相等的,因此需要加上之前的最短路径的条数即 ans[v]+=ans[u]。

通俗来说:
到达 u 的时候有三条路径都是最短的,则ans[u]=3,则接着从 u到达v 的时候,则我们到达v的路径条数是 ans[v]=3

注意ans[1] 初始化为1,1到1默认有一条路径

过程如下:

  1. 点1到点2有一条路径,ans[2] =1
  2. 点1到点3有一条路径,ans[3] =1
  3. 点1到点4有两条路径,1->2->4 和 1-> 3->4 ,ans[4] =2
  4. 点1到点5有四条路径,1->2->4->5(两条),1->3->4->5(两条)ans[5] =4
//TODO: Write code here
int n,m;
const int N=5e6+10,mod=100003;
int nums[N],ans[N];
struct Edge
{
    int to,next;
}edge[N];
int head[N],cnt,dis[N];
void add_edge(int u,int v)
{
    edge[++cnt].next=head[u];
    edge[cnt].to=v;
    head[u]=cnt;
}
void dijktra()
{
    memset(dis,INF,sizeof(dis));
    priority_queue<PI,v<PI>,greater<PI>> q;
    dis[1]=0;
    ans[1]=1;
    q.push({0,1});
    while (!q.empty())
    {
        auto p=q.top();
        q.pop();
        int u=p.second;
        if (dis[u]<p.first) continue;
        for (int i=head[u];i;i=edge[i].next)
        {
            int v=edge[i].to;
            if (dis[v]>dis[u]+1) //边权为1
            {
                dis[v]=dis[u]+1;
                ans[v]=ans[u];
                q.push({dis[v],v});
            }
            else if (dis[v]==dis[u]+1)
            {
                ans[v]+=ans[u];
                ans[v]%=mod;
            }
        }
    }
}
signed main()
{
	cin>>n>>m;
    for (int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge(u,v);
        add_edge(v,u);//双向边
    }
    dijktra();
    for (int i=1;i<=n;i++)
    {
        cout<<ans[i]<<endl;
    }
#define one 1
	return 0;
}

1488. 最短距离

1488. 最短距离

题目要求:给出一些城市(节点),每个城市到另一城市可能具有一条双向道路,道路存在长度,并且某个城市可能存在商店,求出每一个城市到最近的商店的距离是多少。


这道题貌似就是一个简单的最短路的问题:

直接建图,然后dijkstra求就好了,对于每一个询问 i 我们都进行一次dijkstra,寻找i到每一个点的距离最小值,然后再统计到商店的最短的距离。

但是题目的数据范围是点数:105;询问:105。

如果每一次询问都进行一次求最短路的过程,则这远远的超过了时间的限制。

则我们考虑一下:如果不能对于每次询问都求最短的距离,那么很显然我们只需要进行一次求最短路就可以了。

我们思考如何只进行一次求最短路,就可以得到所有点到最近商店的距离。

不妨创建一个超级源点,只对这个超级原点求一次最短路。

设超级原点为 0 点,我们所有的正确的点都是从1开始的,并且把超级原点与所有的商店点相连,边权为0

这样我们从超级原点开始只进行一次最短路

寻找的距离结果如下:

则其实就是可以看作从 商店的节点2处往 其他的所有点做了一次最短路的搜索

1

由于从超级原点到商店的点的边权为0,由于我们让超级原点把所有的商店点都相连了,所以从超级原点开始只需要做一次最短路的搜索就等于从所有的商店点分别做最短路的搜索。

超级原点的引入就解决了 多源最短路问题转换为单源最短路问题

而这个问题的转换关键就是:

  • 超级原点的引入,连接所有的商店点,对超级原点做一次最短路,就等于对所有商店点都做一次最短路,他们两者答案是等价的,因为 超级原点到所有商店点的距离为0

#include <iostream>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
#include <limits.h>
using namespace std;
const int N=1e5+10;
struct Edge{
    int next,to,w;
}edge[N<<2];
int head[N],cnt;
int n,m,k,q,dis[N];
void add_edge(int u,int v,int w){
    edge[++cnt].next=head[u];
    edge[cnt].to=v;
    edge[cnt].w=w;
    head[u]=cnt;
}
void dijkstra(){
    priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q;
    q.push({0,0}); //从超级原点开始做一次最短路即可
    dis[0]=0;
    while (!q.empty()){
        auto p=q.top();
        q.pop();
        int u=p.second;
        if (dis[u]<p.first) continue;
        for (int j=head[u];j;j=edge[j].next){
            int v=edge[j].to;
            if (dis[v]>dis[u]+edge[j].w){  // dis[v]>dis[u]+edge[j].w
                dis[v]=dis[u]+edge[j].w;
                q.push({dis[v],v});
            }
        }
    }
}
signed main(){
    cin>>n>>m;
    memset(dis,0x3f,sizeof(dis));
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w);
        add_edge(v,u,w);
    }
    cin>>k;
    while (k--){
        int x;
        scanf("%d",&x);
        add_edge(0,x,0); // 增加一条超级原点到目标点(商店)的边,单向边,w=0
    }
    dijkstra(); 
    cin>>q;
    while (q--){
        //询问编号为x的村庄与其距离最近的商店之间的距离。
        int x;
        scanf("%d",&x);
        printf("%d\n",dis[x]);
    }
    
    return 0;
}

3305. 作物杂交

3305. 作物杂交

题目要求:给出n种作物的成熟时间,并且规定某些作物两两之间可以进行杂交,即A和B生成C,杂交的时间为A和B两种作物的时间的最大值,给定一些起始种子,求出最后得到目标种子的最短时间

N:N种子各自的成熟时间

M:已经拥有的初始种子

K: 杂交过程:A和B生成C

T: 目标种子


每一次两个作物的杂交生成另一个种子,我们看作一次操作

不妨假设 F(i,C) 表示经过了 i 次操作,生成C种子的最短时间

注意L:这个F本身就表示一个最短的时间

由于最后我们一定会得到目标种子T,因此思考在总共进行 i 次操作的情况下:

  • 最后的一次操作一定是第 i 次,并且这最后一次操作的时间一定是两者的时间的最大值:max(nums[A],nums[B])
  • 对于前 i-1 次操作,即 F(i-1,A)和 F(i-1,B)我们一定要取得一个最大值,即 max( F[i-1,A], F[i-1,B])

不妨把 F(i,C)表示的最短时间看作是最短路径。

即寻找一条使得从起始点到目标点的最短路径:

  • dis[C]=max(dis[A],dis[B])+max(w[A],w[B])

对于任意的C点我们都可以表示为第 i 次操作A和B的杂交时间 + 前i-1次生成A和B的杂交时间

那么这一个总的过程我们就可以看作是:寻找从起始点到最终点的最短路径,只不过这个最短路径是由他们的杂交时间维护的。


#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int nums[N];  //每个作物的时间
int n,m,k,t;
struct Edge{
    int to,next,B;
}edge[N];
int head[N],cnt;
queue<int> q;
bool vis[N];
int dis[N];
void add_edge(int A,int B,int C){
    edge[++cnt].to=C;
    edge[cnt].B=B;
    edge[cnt].next=head[A];
    head[A]=cnt;
}
void spfa(){
    while (!q.empty()){
        int A=q.front();
        //对于A点来说
        q.pop();
        vis[A]=false;
        for (int j=head[A];j;j=edge[j].next){
            //寻找A的相邻的点,即寻找B点与C点:A+B->C
            int B=edge[j].B,C=edge[j].to;
            //当前生成C的最短路径就是前i-1次操作和第i次操作的时间
            if (dis[C]>max(dis[A],dis[B])+max(nums[A],nums[B])){
                dis[C]=max(dis[A],dis[B])+max(nums[A],nums[B]);
                //spfa松弛
                if (!vis[C]){
                    vis[C]=true;
                    q.push(C);
                }
            }
        }
    }
}
int main(){
    cin>>n>>m>>k>>t;
    for (int i=1;i<=n;i++){
        //每个作物的种植时间
        cin>>nums[i];
    }
    memset(dis,0x3f,sizeof(dis));
    while (m--){
        //初始的作物
        int x;
        cin>>x;
        //记录源点(多源)
        q.push(x);
        dis[x]=0;
        vis[x]=true;
    }
    while (k--){
        //a和b生成c
        int a,b,c;
        cin>>a>>b>>c;
        add_edge(a,b,c);
        add_edge(b,a,c);
    }
    spfa();
    printf("%d\n",dis[t]);
    return 0;
}

4074. 铁路与公路

4074. 铁路与公路

题目要求:对于n个城市两两之间:如果有铁路则一定没有公路,如果有公路则一定没有铁路。他们都是双向的,一列火车和汽车同时从起点出发,两者最后到达目标地点的时间是多少(为两者之间最后到达者的时间),求得最小值。


这道题目给出的点数较少,只有最多 400个,可以考虑使用 floyd算法,时间复杂度为 O(N^3)

考虑如果求得铁路和公路的到达时间最大值?

由于规定了经过每条铁路或公路都需要花费 1 小时的时间

因为两个道路不一样,并且不重复,并且火车和汽车不能混走道路。

则一定存在:

  • 铁路从1到n得时间为 t=1,则公路一定 t>1,最终时间为t公路
  • 铁路从1到n得时间为 t>1,则公路一定 t=1,最终时间为t铁路

因此我们只需要分别计算两次最短路求他们两者的时间最大值即可

对于 i 到 j 是铁路的道路,我们标记 i 到 j 为true,则下面我们便可以使得 i 到 j 为公路。

因为两个城市不是公路就是铁路

同时需要注意双向道路

另外 spfa和dijkstra也可以做这道题目:

floyd算法运行 5344ms

dijkstra算法运行 264 ms

#include <bits/stdc++.h>
using namespace std;
#define INF 999999999
const int V=410; //点数
bool fg[V][V];
int dis[V][V];
int n,m;
void init(){
    for (int i=1;i<=n;i++){
        for (int j=1;j<=n;j++){
            dis[i][j]=INF;
        }
    }
}
void floyd(){
    for (int k=1;k<=n;k++){
        for (int i=1;i<=n;i++){
            for (int j=1;j<=n;j++){
                dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
            }
        }
    }
}
signed main(){
    cin>>n>>m;
    init();
    for (int i=1;i<=n;i++) {dis[i][i]=0;}
    for (int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        fg[u][v]=fg[v][u]=true;
        dis[u][v]=dis[v][u]=1;
    }
    floyd();
    int ans1=dis[1][n]; //铁路
    
    //重置
    init();
    for (int i=1;i<=n;i++) {dis[i][i]=0;}
    for (int i=1;i<=n;i++){
        for (int j=i+1;j<=n;j++){
            if (!fg[i][j]){
                fg[i][j]=fg[j][i]=true;
                dis[i][j]=dis[j][i]=1;
            }
        }
    }
    floyd();
    int ans2=dis[1][n];//公路
    
    if (ans1==INF || ans2==INF){
        cout<<-1<<endl;
        return 0;
    }
    cout<<max(ans1,ans2);
    return 0;
}
posted @ 2023-04-01 22:36  hugeYlh  阅读(77)  评论(0编辑  收藏  举报