最短路算法

最短路算法


简述

最短路是一种及常见的算法,在OI考试及平常生活中,都很常见,也是图论学习的初步算法。ios

牢固掌握最短路算法,是极为重要的。算法

常见的最短路算法有如下几种:数组

Floyd算法网络

  • 多源最短路,求出全部点对的最短路长度
  • 时间复杂度:\(O(n³)\)

Dijkstra算法闭包

  • 单源最短路,求出某个点s到全部点的最短路长度
  • 时间复杂度:\(O(n²)/O(m log n)\)
  • 没法处理负权

SPFA算法,即队列优化的Bellman-Ford算法学习

  • 单源最短路,求出某个点s到全部点的最短路长度
  • 时间复杂度:声称为\(O(m)\),最坏\(O(nm)\),容易卡到最坏
  • 能够处理负权边,能够判断负权环

松弛操做

松弛操做:经过某条路径更新dis[v]的值优化

  • $if (dis[v] > dis[u] + e.dist) dis[v] = dis[u] + e.dist $
  • 尝试使用s到u的最短路加上边(u,v)的长度来更新s到v的最短路

几乎是全部最短路(单源)算法的核心。spa


Floyd算法

算法原理

Floyd算法是一个经典的动态规划算法code

用通俗的语言来描述的话,首先咱们的目标是寻找从点i到点j的最短路径。

从动态规划的角度看问题,咱们须要为这个目标从新作一个诠释。(这个诠释正是动态规划最富创造力的精华所在)

实现方式

从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i通过若干个节点k到j。

因此,咱们假设\(Dis(i,j)\)为节点u到节点v的最短路径的距离,

对于每个节点k,咱们检查\(Dis(i,k) + Dis(k,j) < Dis(i,j)\)是否成立,

若是成立,证实从i到k再到j的路径比i直接到j的路径短,咱们便设置\(Dis(i,j) = Dis(i,k) + Dis(k,j)\)

这样一来,当咱们遍历完全部节点k,\(Dis(i,j)\)中记录的即是i到j的最短路径的距离。

代码实现

只有5行,简单易懂,但新手容易写错的地方是枚举顺序,必定是先中间节点\(k\),再枚举\(i,j\)

1 for(k=1;k<=n;k++)
2     for(i=1;i<=n;i++)
3         for(j=1;j<=n;j++)
4             if(e[i][j]>e[i][k]+e[k][j])
5                  e[i][j]=e[i][k]+e[k][j];

传递闭包

在交际网络中,给定若干个元素和若干个二元对关系,且关系具备传递性,

“经过传递性推导出更多的元素之间的关系” 被称为传递闭包。

创建邻接矩阵,d,其中 \(d(i,j)=1\) 表示 \(i\)\(j\) 有关系,\(d(i,j)=0\) 表示 \(i\)\(j\) 没有关系,特别的,\(d(i,i)=1\)

使用 Floyd 算法能够解决传递闭包问题,代码以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#define maxn 330
using namespace std;

int n,m;
bool d[maxn][maxn];

int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++) d[i][i]=true;
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d %d",&x,&y);
        d[x][y]=d[y][x]=true;
    }
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                d[i][j]|=d[i][k]&d[k][j];//核心代码
            }
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(d[i][j]) printf("%d %d\n",i,j);
        }
    }
    return 0;
}

这样就完美解决了这个问题(没学以前,有一次老师出了原题,我考试后自闭了)

实际应用

因为Floyd的时间复杂度并不优秀,它在实际应用中每每只起到思想启蒙的做用。

咱们能够用Floyd的思想来计算一些题目。(一般只要发现是Floyd的思想,代码实现十分简单)

固然若是是须要模板题的话,能够看这里

想要加深理解的话,看这道题


Dijkstra算法

算法原理

本算法基于贪心思想,并不适用于有负权图中

设G=(V,E)是一个带权有向图,把图中顶点集合V分红两组,第一组为已求出最短路径的顶点集合。

(用S表示,初始时S中只有一个源点,之后每求得一条最短路径 , 就将加入到集合S中,直到所有顶点都加入到S中)

第二组为其他未肯定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。

在加入的过程当中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。

此外,每一个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度。

U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。

以上原理没看懂没有关系,主要是如下的实现方式

实现方式

  1. 初始化\(dist[1]=0\),其他节点的 \(dist\) 的值为正无穷大。
  2. 找出一个未被标记的点、\(dist[x]\) 最小的节点 \(x\) ,而后标记节点 \(x\)
  3. 扫描节点 \(x\) 的全部出边 \((x,y,z)\) ,若 \(dist[y]>dist[x]+z\) ,则使用 \(dist[x]+z\) 更新 \(dist[y]\)
  4. 重复以上 2,3 两个步骤,直到全部节点都被标记。

代码实现

未加优化的算法以下,时间复杂度 \(O(n^2)\)

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#define N 10010
#define M 500010
#define maxd 2147483647
using namespace std;

int n,m,s,dis[N];
bool use[N];
int head[M],cnt=0;
struct node{
    int next,to,val;
}edge[M];

void addedge(int x,int y,int z){
    cnt++;
    edge[cnt].next=head[x];
    edge[cnt].to=y;
    edge[cnt].val=z;
    head[x]=cnt;
    return;
}

void dij(){
    for(int i=1;i<=n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    dis[s]=0;

    for(int i=2;i<=n;i++){
        int minn=maxd,k;
        for(int j=1;j<=n;j++){
            if(!use[j]&&minn>dis[j]){minn=dis[j];k=j;}
        }//寻找全局最小值
        use[k]=true;
        for(int j=head[k];j;j=edge[j].next){
            int go=edge[j].to;
            if(use[go]) continue;
            dis[go]=min(dis[go],dis[k]+edge[j].val);
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int u,v,w;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&u,&v,&w);
        addedge(u,v,w);
    }
    dij();
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    //system("pause");
    return 0;
}

那么咱们考虑怎么优化呢?

咱们能够发现,上面程序的主要瓶颈在于寻找全局最小值的过程(见注释)

因此咱们能够用一个小根堆进行维护,用 \(O(log n)\) 的时间获取最小值,并用 \(O(log n)\) 的时间执行一条边的扩展更新,

最终在 \(O(m log n)\) 的时间内完成算法,代码以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#include<queue>
#define N 100010
#define M 1000010
#define maxd 2147483647
using namespace std;

int n,m,s;
int head[N],dis[N],cnt=0;
bool use[N];
struct node{
    int next,to,val;
}edge[M];
priority_queue<pair<int,int> >q;
//为了不重载小于号。
//pair的first用于存储dis[]的相反数(变小根堆),second用存储编号。

void addedge(int x,int y,int z){
    cnt++;
    edge[cnt].next=head[x];
    edge[cnt].to=y;
    edge[cnt].val=z;
    head[x]=cnt;
    return;
}

void dij(){
    for(int i=1;i<=n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    q.push(make_pair(0,s));
    dis[s]=0;

    while(!q.empty()){
        int now=q.top().second;
        q.pop();
        if(use[now]) continue;
        use[now]=true;
        for(int i=head[now];i;i=edge[i].next){
            int y=edge[i].to;
            int z=edge[i].val;
            if(dis[y]>dis[now]+z){
                dis[y]=dis[now]+z;
                q.push(make_pair(-dis[y],y));
            }
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int u,v,w;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&u,&v,&w);
        addedge(u,v,w);
    }
    dij();
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    //system("pause");
    return 0;
}

实际应用

因为时间复杂度出众,Dijkstra算法是单源最短路的经常使用解法之一(前提是没有负权)

舒适提示:若是你既能够用Dijkstra,又能够用\(SPFA\) ,请不要选择\(SPFA\) ,否则你将承担十年OI一场空的风险

应用范围很广,常常在题目中遇到,必定要紧紧掌握。


SPFA算法

算法原理

\(SPFA\) 特殊之处在于它是一个基于队列的最短路算法。

它的原理是对图进行V-1次松弛操做,获得全部可能的最短路径。

优势是边的权值能够为负数、实现简单。缺点是容易被卡。

实现方式

因为它是队列优化的 Bellman-Ford 算法,因此咱们先介绍 Bellman-Ford 算法。

Bellman-Ford 算法基于迭代思想。它的流程以下:

  1. 扫描全部的边 \((x,y,z)\) ,若 \(dist[y]>dist[x]+z\) 则用 \(dist[x]+z\) 更新 \(dist[y]\)
  2. 重复上述步骤,直到没有更新操做产生。

时间复杂度为 \(O(nm)\)

\(SPFA\) 算法流程以下:

  1. 创建一个队列,最初队列中只含有起点1。
  2. 取出队头节点 \(x\) ,扫描它的全部出边\((x,y,z)\),若 \(dist[y]>dist[x]+z\) 则用 \(dist[x]+z\) 更新 \(dist[y]\)
  3. 同时,若是 \(y\) 不在队列中,则把 \(y\) 入队。
  4. 重复 2~3​ 步,直至队列为空。

是否是十分简单易懂?

这个队列避免了 Bellman-Ford 算法中对不须要扩展的节点的冗余扫描,在稀疏图中的效率极高。

时间复杂度很玄学,为 \(O(km)\) 级别,其中能够证实,通常 \(k\leq2\)

可是在稠密图或特殊构造的网络图(良心出题人),该算法可能退化成 \(O(nm)\)

因此没有负权时用Dijkstra算法,有负权时用 \(SPFA\) 算法(这个时候他还卡你 \(SPFA\) 就没道理了)

代码实现

普通的 \(SPFA\) 算法实现简单,代码以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<cstring>
#include<queue>
#define N 100010
#define M 1000010
#define maxd 2147483647 
using namespace std;

int n,m,s;
int head[N],cnt=0,dis[N];
bool use[N];
struct node{
    int next,to,val;
}edge[M];
queue<int>q;

void addedge(int x,int y,int z){
    cnt++;
    edge[cnt].next=head[x];
    edge[cnt].to=y;
    edge[cnt].val=z;
    head[x]=cnt;
    return;
}

void spfa(){
    for(int i=1;i<=n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    use[s]=true;
    dis[s]=0;
    q.push(s);

    while(!q.empty()){
        int now=q.front();q.pop();
        use[now]=false;
        for(int i=head[now];i;i=edge[i].next){
            int y=edge[i].to;
            int z=edge[i].val;
            if(dis[y]>dis[now]+z){
                dis[y]=dis[now]+z;
                if(!use[y]) q.push(y),use[y]=true;
            }
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int u,v,w;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&u,&v,&w);
        addedge(u,v,w);
    }
    spfa();
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    //system("pause");
    return 0;
}

优化固然是有的,可是因为优化价值不大,甚至更容易被卡掉,因此这里就不介绍了。

若是要判断负环,仅须要计算每个点的入队状况,若是某个点松弛了第n次,说明有负环。

实际应用

这个代码主要用于负权图中,稀疏图表现也还行。

若是你担忧有负权但出题人又卡 \(SPFA\) 的话,请自行寻找出路(其实应该不会出现这种状况)


分层图最短路

前置知识

分层图最短路是指在能够进行分层图的图上解决最短路问题。(分层图:能够理解为有多个平行的图)

通常模型是:在一个正常的图上能够进行 k 次决策,对于每次决策,不影响图的结构,只影响目前的状态或代价。

通常将决策前的状态和决策后的状态之间链接一条权值为决策代价的边,表示付出该代价后就能够转换状态了。

通常有两种方法解决分层图最短路问题:

  1. 建图流:建图时直接建成k+1层。
  2. 升维流:多开一维记录机会信息。

固然具体选择哪种方法,看数据范围吧 。

方法一

咱们建k+1层图。而后有边的两个点,多建一条到下一层边权为0的单向边,若是走了这条边就表示用了一次机会。

有N个点时,1~n表示第一层, (1+n)~(n+n)表明第二层, 以此类推。

由于要建K+1层图,数组要开到n * ( k + 1),点的个数也为n * ( k + 1 ) 。

请看代码:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 5e4 * 42;
using namespace std;
struct node {int to,w,next;} edge[maxn];
int head[maxn], cnt;
int dis[maxn], vis[maxn];
int n, m, s, t, k;
struct Dijkstra
{
    void init()
    {
        memset(head,-1,sizeof(head));
        memset(dis,0x3f,sizeof(dis));
        memset(vis,0,sizeof(vis));
        cnt = 0;
    }
 
    void add(int u,int v,int w)
    {
        edge[cnt].to = v;
        edge[cnt].w = w;
        edge[cnt].next = head[u];
        head[u] = cnt ++;
    }
 
    void dijkstra()
    {
        priority_queue<pii,vector<pii>,greater<pii> > q;
        dis[s] = 0; q.push({dis[s],s});
        while(!q.empty())
        {
            int now = q.top().second;
            q.pop();
            if(vis[now]) continue;
            vis[now] = 1;
            for(int i = head[now]; i != -1; i = edge[i].next)
            {
                int v = edge[i].to;
                if(!vis[v] && dis[v] > dis[now] + edge[i].w)
                {
                    dis[v] = dis[now] + edge[i].w;
                    q.push({dis[v],v});
                }
            }
        }
    }
}dj;
 
int main()
{
    while(~scanf("%d%d%d", &n, &m, &k))
    {
        dj.init(); scanf("%d%d",&s,&t);
        while(m--)
        {
            int u, v, w;
            scanf("%d%d%d",&u, &v, &w);
            for(int i = 0; i <= k; i++)
            {
                dj.add(u + i * n, v + i * n, w);
                dj.add(v + i * n, u + i * n, w);
                if(i != k)
                {
                    dj.add(u + i * n, v + (i + 1) * n, 0);
                    dj.add(v + i * n, u + (i + 1) * n, 0);
                }
            }
        }
        dj.dijkstra(); int ans = inf;
        for(int i = 0; i <= k; i++)
            ans = min(ans, dis[t + i * n]);
 
        printf("%d\n",ans);
    }
}

方法二

咱们把dis数组和vis数组多开一维记录k次机会的信息。

dis[ i ][ j ] 表明到达 i 用了 j 次免费机会的最小花费.
vis[ i ][ j ] 表明到达 i 用了 j 次免费机会的状况是否出现过.

更新的时候先更新同层之间(即花费免费机会相同)的最短路,而后更新从该层到下一层(即再花费一次免费机会)的最短路。

不使用机会 dis[v][c] = min(min,dis[now][c] + edge[i].w);
使用机会 dis[v][c+1] = min(dis[v][c+1],dis[now][c]);

写法相似于 \(DP\)

代码见下:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 1e5+7;
using namespace std;
struct node{int to, w, next, cost; } edge[maxn];
int head[maxn], cnt;
int dis[maxn][15], vis[maxn][15];
int n, m, s, t, k;
struct Dijkstra
{
    void init()
    {
        memset(head,-1,sizeof(head));
        memset(dis,127,sizeof(dis));
        memset(vis,0,sizeof(vis));
        cnt = 0;
    }
 
    void add(int u,int v,int w)
    {
        edge[cnt].to = v;
        edge[cnt].w = w;
        edge[cnt].next = head[u];
        head[u] = cnt ++;
    }
 
    void dijkstra()
    {
        priority_queue <pii, vector<pii>, greater<pii> > q;
        dis[s][0] = 0;
        q.push({0, s});
        while(!q.empty())
        {
            int now = q.top().second; q.pop();
            int c = now / n; now %= n;
            if(vis[now][c]) continue;
            vis[now][c] = 1;
            for(int i = head[now]; i != -1; i = edge[i].next)
            {
                int v = edge[i].to;
                if(!vis[v][c] && dis[v][c] > dis[now][c] + edge[i].w)
                {
                    dis[v][c] = dis[now][c] + edge[i].w;
                    q.push({dis[v][c], v + c * n});
                }
            }
            if(c < k)
            {
                for(int i = head[now]; i != -1; i = edge[i].next)
                {
                    int v = edge[i].to;
                    if(!vis[v][c+1] && dis[v][c+1] > dis[now][c])
                    {
                        dis[v][c+1] = dis[now][c];
                        q.push({dis[v][c+1], v + (c + 1) * n});
                    }
                }
            }
        }
    }
}dj;
 
int main()
{
    while(~scanf("%d%d%d", &n, &m, &k))
    {
        dj.init(); scanf("%d%d",&s,&t);
        while(m--)
        {
            int u, v, w;
            scanf("%d%d%d",&u, &v, &w);
            dj.add(u, v, w);
            dj.add(v, u, w);
        }
        dj.dijkstra();
        int ans = inf;
        for(int i = 0; i <= k; i++)
            ans = min(ans, dis[t][i]);
        printf("%d\n", ans);
    }
}

可见本写法较为复杂(由于我不会),因此推荐第一种作法。

你会发现讲述时常常用到 “机会”这个词,缘由是这道题

能够当作模板题了。


有点权的最短路

通常的最短路并无点权,但若是遇到点权怎么办呢?

  1. 思路一:先无论点权,走到一个点以后再加,结果发现不可行。
  2. 思路二:上面提到的分层思想,将点权化为边权,发现可行。

具体流程以下:

  1. \(n\) 个点的图分为两层,共计 \(2n\) 个点 。
  2. 输入点权,将点 \(i\) 与点 \(n+i\) 相连,边权为点权。
  3. 输入边权,假设为 \((u,v,w)\) ,那么将点 \(u+n\) 与点 \(v\) 相连,边权为 \(w\)
  4. 跑一遍共 \(2n\) 个点的最短路。
  5. 输出 \(dis(n+1...n+n)\) 即为答案。

代码以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#include<queue>
#include<vector>
#define N 100010
#define M 1000010
#define maxd 2147483647
using namespace std;

int n,m,s,dis[2*N];
bool use[2*N];
struct node{
    int to,val;
};
vector<node>edge[2*N];
priority_queue<pair<int,int> >q;

void dij(){
    for(int i=1;i<=2*n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    q.push(make_pair(0,s));
    dis[s]=0;

    while(!q.empty()){
        int now=q.top().second;
        q.pop();
        if(use[now]) continue;
        use[now]=true;
        for(int i=0;i<edge[now].size();i++){
            int y=edge[now][i].to;
            int z=edge[now][i].val;
            if(dis[y]>dis[now]+z){
                dis[y]=dis[now]+z;
                q.push(make_pair(-dis[y],y));
            }
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int a,b,c;
    node p;
    for(int i=1;i<=n;i++){
        scanf("%d",&a);
        p.to=i+n;
        p.val=a;
        edge[i].push_back(p);
    }
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&a,&b,&c);
        p.to=b;
        p.val=c;
        edge[a+n].push_back(p);
    }
    dij();
    for(int i=n+1;i<=2*n;i++) printf("%d ",dis[i]);
    system("pause");
    return 0;
}

代码使用 \(dij\) 实现,其余实现方法大同小异。

虽然运用并非很广,但这里提到一下,之后可能会用的到。


结语

这么重要的算法怎么能不学呢?

全文资料:《算法竞赛进阶指南》以及某谷 \(dalao\) 的博客

熟练掌握三种基础算法以后,必定要灵活应用。

咕咕咕

相关文章
相关标签/搜索