第三关 ——图论:最短路

Posted wybxz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第三关 ——图论:最短路相关的知识,希望对你有一定的参考价值。

14:52:09 今天天气好晴朗,处处好风光。

阳光犹如希望,我即将要回去打游戏的希望,希望那么那么大,那么那么刺眼!!!

好的,接下来为您播报考试后续情况:不算太差,还看得过去,至少,应该能活着把年过过去。接下来,就介绍一下在父母看到成绩后想要打你时如何通过最短路逃跑

目录

  • Dijkstra算法

  • Bellman-Ford算法

  • SPFA算法

  • Floyd算法

首先,要来简单论述一下“最短路”这个问题。要求最短路,无论什么方法,都要先存图。

百“路”图为先

 那么,图分为两种,有向图与无向图。无论是有向图还是无向图,存图方式都是一样的。分为两种:

  • 邻接矩阵(空间复杂度:O(n2))

可以说,矩阵存图是非常非常的简单易打了。话不多说,放代码

int n,m;
    cin>>n>>m;
    memset(e,127,sizeof(e));//将矩阵中所有边初始化为无穷大
    for(int i=1; i<=n; i++) e[i][i]=0;
    for(int i=1; i<=m; i++) {
        int x,y,z;
        cin>>x>>y>>z;//x点与y点有一条边,边的权值为z
        e[x][y]=z;
        e[y][x]=z;
    }
  • 邻接表(空间复杂度:O(n+m))

邻接表可以用数组模拟链表指针:

void add(int x,int y,int z)//x点与y点有一条边,边的权值为z
{
    ver[++tot]=y,edge[tot]=z; 
    next[tot]=head[x],head[x]=tot; 
} 
for(int i=head[x];i;i=next[i])
{
    int y=ver[i],z=edge[i]; 
}//于主函数中

也可以用vector存储:

vector<int>e[1000];//vector代替邻接表
for(int i=1;i<=m;i++)
{
    int x,y,z;
    scanf("%d%d",&x,&y);//x,y两点间有一条边 
    e[x].push_back(y);//加入x的vector
}

好的,接下来开始讲我们的单源最短路径的几种算法(单源最短路径:源点到每个点的距离)

  • Dijkstra算法(基于贪心思想,适用于没有负权的图)

算法流程

  1. 初始化dis[1]=0,其余dis数值为正无穷大
  2. 找到未被标记的最小节点x,并标记
  3. 扫描x的所有出边,若dis[y]>dis[x]+z(边权),则用dis[x]+z更新dis[y]的值
  4. 直到节点被标记完为止

dis[x]代表源点到x的最短路径。

它的时间复杂度为(O(logn))

单源最短路问题

题目描述

输入一个无向网络,输出其中2个顶点之间的最短路径长度

输入

输入文件第一行为n和m,表示有n个顶点和m条带权边,其中顶点编号是从1到n,接下来有m行,每行三个整数,分别表示两个顶点编号和对应边的权值,再接下来有一行,两个整数表示要求的最短路径的两个顶点编号

输出

输出文件就一行,即两个顶点间的最短路径长度(权值和)

输入样例

4 5
1 2 2
1 3 1
2 3 2
2 4 1
3 4 6
1 4

输出样例

3

这里需要用vis数组存储是否标记。

#include<bits/stdc++.h>
using namespace std;
int e[1000][1000],dis[1000],vis[1000];
int main() {
    int n,m;
    cin>>n>>m;
    memset(e,127,sizeof(e));
    for(int i=1; i<=n; i++) e[i][i]=0;
    for(int i=1; i<=m; i++) {
        int x,y,z;
        cin>>x>>y>>z;
        e[x][y]=z;
        e[y][x]=z;
    }
    int s,b;
    cin>>s>>b;
    for(int i=1; i<=n; i++) {
        dis[i]=e[s][i];
    }
    vis[s]=1;
    for(int i=1; i<=n; i++) {
        int mmin=99999,k;
        for(int j=1; j<=n; j++) {
            if(vis[j]!=1) {
                if(dis[j]<mmin) {
                    mmin=dis[j];
                    k=j;
                }
            }
        }
        vis[k]=1;
        for(int j=1; j<=n; j++) {
            dis[j]=min(dis[j],dis[k]+e[k][j]);
        }
    }
    cout<<dis[b];
    return 0;
}

Dijkstra堆优化(时间复杂度(O(n2)))

堆排序

#include<iostream>
#include<queue>
using namespace std;
priority_queue<int, vector<int>, greater<int> >q;
int main(){
    int n,x;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x;
        q.push(x);
    }
    while(!q.empty()){
        cout<<q.top()<<" ";
        q.pop();
    }
    return 0;
} 

https://www.luogu.com.cn/problem/P4779

所以就有了堆优化的Dijkstra

堆中每个元素存两个值 结点编号xx和入堆是该点被更新成的距离disdis
disdis为第一关键字(即按dis的小根堆) 主要思想不变 每次找距离最小点是直接取出堆顶并删除堆顶
更新最短路的时候 将更新后的节点入堆

注意一个点可能会被更新多次而入堆多次 但是只有最后一次入堆才是正确的dis 同时也一定是在所有入堆操作结束后才会出堆 所以直接开个数组判断有没有出堆过就好了 当然也可以根据堆中元素的距离大小和点的最短路大小直接判断

#include<bits/stdc++.h>
#define M(x,y) make_pair(x,y)
using namespace std;
int fr[100010],to[200010],nex[200010],v[200010],tl,d[100010];
bool b[100010];
void add(int x,int y,int w){
    to[++tl]=y;
    v[tl]=w;
    nex[tl]=fr[x];
    fr[x]=tl;
}
priority_queue< pair<int,int> > q;
int main(){
    int n,m,x,y,z,s;
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
    }
    for(int i=1;i<=n;i++) d[i]=1e10;
    d[s]=0;
    q.push(M(0,s));
    while(!q.empty()){
        int x=q.top().second;
        q.pop(); 
        if(b[x]) continue;
        b[x]=1;
        for(int i=fr[x];i;i=nex[i]){
            int y=to[i],l=v[i];
            if(d[y]>d[x]+l){
                d[y]=d[x]+l;
                q.push(M(-d[y],y));
            }
        }
    }
    for(int i=1;i<=n;i++) printf("%d ",d[i]);
    return 0;
}
  • Bellman-Ford算法和SPFA算法

Bellman-Ford算法

算法流程:

  1. 扫描所有边(x,y,z),若dis[y]>dis[x]+z(边权),则用dis[x]+z更新dis[y]的值
  2. 重复直到没有更新
  3. 时间复杂度(O(nm))

SPFA算法

算法流程:

  1. 建立一个队列,最初队列只含起点1
  2. 取队头结点x,扫描它的所有出边,若dis[y]>dis[x]+z(边权),则用dis[x]+z更新dis[y]的值
  3. 若y不在队列中的话,将y入队
  4. 重复直到队列为空
  5. 时间复杂度(O(km))
  6. 可解决负权,也可判断有无负环

可惜的是,该算法已死。

http://noi-test.zzstep.com/contest/0x60%E3%80%8C%E5%9B%BE%E8%AE%BA%E3%80%8D%E4%BE%8B%E9%A2%98/6101%20%E6%9C%80%E4%BC%98%E8%B4%B8%E6%98%93

以1为起点,在原图上使用SPFA算法,用数组d[x]表示在原图中从节点1到节点x的所有路径中,能够经过的权值最小的节点的权值

以n为起点,在反图上使用SPFA算法,用数组f[x]表示在原图中从节点x到节点n的所有路径中(反图中是n到x),能够经过的权值最大的节点的权值

最后,枚举每个节点x,用f[x]-d[x]更新答案

#include<bits/stdc++.h>
using namespace std;
const int N=100002;
int a[N],b[N],c[N];
vector<int> g[N];
vector<int> s[N];
queue<int> q;
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;++i)
    cin>>a[i];
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        cin>>x>>y>>z;
        g[x].push_back(y);
        s[y].push_back(x);
        if(z==2)
        {
            g[y].push_back(x);
            s[x].push_back(y);
        }
    }
    q.push(1);
    memset(c,60,sizeof(c));
    c[1]=9999999;
    while(!q.empty())
    {
        int t=q.front();
        q.pop();
        c[t]=min(c[t],a[t]);
        for(int i=0;i<g[t].size();i++)
        if(c[t]<c[g[t][i]])
        c[g[t][i]]=c[t],q.push(g[t][i]);
    }
    q.push(n);
    while(!q.empty())
    {
        int t=q.front();
        q.pop();
        b[t]=max(b[t],a[t]);
        for(int i=0;i<s[t].size();i++)
        if(b[t]>b[s[t][i]])
        b[s[t][i]]=b[t],q.push(s[t][i]);
    }
    int ans=0;
    for(int i=1;i<=n;i++)
    ans=max(ans,b[i]-c[i]);
    cout<<ans;
    return 0;
}

20:12:48 所以暂时将你眼睛闭了起来   ——伍佰

多源最短路径(任意两点间最短路径)

  • Floyd算法

状态转移方程:d[i][j]=min(d[i][j],d[i][k]+d[k][j]);

可用邻接矩阵存图

题目描述

输入一个有向图的邻接矩阵格式,输出每对顶点间的最短路径长度

输入

输入第一行为n,表示下面是个n n的矩阵,接下来就是n n的矩阵形式,每个元素值都是整型,如果不能直接到,则是-1

输出

输出也是一个n * n的矩阵形式,每个元素的值为两个顶点间的最短路径值,如果到不了,则输出-1,最后一行的换行也输出

输入样例

3
0 1 3
2 0 5
3 2 0

输出样例

0 1 3
2 0 5
3 2 0
这是一道模板题,所以直接看代码。
#include<bits/stdc++.h>
using namespace std;
int a[60][60];
int x,y,n;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    {
        cin>>a[i][j];
        if(a[i][j]==-1)
        a[i][j]=9999999;
    }
    
    for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(a[i][j]==9999999)
            a[i][j]=-1;
            cout<<a[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

文化之旅

因为Floyd是插点找最短路,所以每次状态都会被记录,插的点的文化就会被记录在这条路里,比如若在i,j中插入k时没有矛盾且满足最短路,那么将两条路上的信息合并并更新k的信息,就可以得到新路径的值。只需要用Floyd再加上一部分优化就好,只是要想到用Floyd不容易,可能会想到用Dijkstra算法(可是不知道能不能过)

#include<bits/stdc++.h>
using namespace std;
int n,k,m,s,t,u,v,d,c[102],a[102][102],f[102][102];
int main()
{
    cin>>n>>k>>m>>s>>t;
    for(int i=1;i<=n;i++)
    cin>>c[i];
    for(int i=1;i<=k;i++)
    for(int j=1;j<=k;j++)
    cin>>a[i][j];
    memset(f,60,sizeof(f));
    for(int i=1;i<=m;i++)
    {
        cin>>u>>v>>d;
        if(!a[c[v]][c[u]]&&c[v]!=c[u])f[u][v]=min(f[u][v],d);
        if(!a[c[u]][c[v]]&&c[u]!=c[v])f[v][u]=min(f[v][u],d);
    }
    if(c[s]==c[t])
    {
        cout<<-1;
        return 0;
    }
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    {
        if(i!=j)
        for(int k=1;k<=n;k++)
        {
            if(j!=k&&i!=k)
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
        }
    }
    if(f[s][t]>999999)cout<<-1;
    else cout<<f[s][t];
    return 0;
 } 

20:52:02 

我终于要认认真真看电视了!!!

以上是关于第三关 ——图论:最短路的主要内容,如果未能解决你的问题,请参考以下文章

图论最短路径Floyed算法和Dijkstra算法

图论----最短路问题

图论:图的四种最短路径算法

图论:图的四种最短路径算法

运筹学-图论实例

运筹学-图论实例