最短路径

来源:互联网 发布:关系数据库语言 编辑:程序博客网 时间:2024/05/16 13:07

        很久没有练习最短路径的题目了,本以为掌握的不错,却忽略了每种算法都有它的拓展,它的变式,并非所有的题目都是让你求个最短路径那么简单(即使在一般的比赛中spfa也仅仅是用来求最短路径)。刷了几天的水题,也是时候做些总结了。

        众所周知,最短路径的算法一共有四种,分别是(ASAP)Floyed,(SSSP)Dijkstra,(SSSP)BellmanFord,(SSSP)SPFA。个人认为SPFA仅仅是BellmanFord的强化版,应该归属于同一种算法。因此,以下只总结前三种算法。


Floyed算法:

      Floyed算法较为简单,本质就是一个DP来求多源最短路,由于代码简单,常被用于距离的预处理中。不多说,先上标准代码

#include <cstdio>#include <cstring>#include <iostream>using namespace std;const int MAXN = 101;int n,m,s,t;int map[MAXN][MAXN];int main(){    memset(map,127,sizeof(map));//距离赋为无穷大     scanf("%d%d",&n,&m);    for (int i=1;i<=m;i++)    {        int from,to,dist;        scanf("%d%d%d",&from,&to,&dist);        map[from][to] = dist;    }    scanf("%d%d",&s,&t);    //Floyed    for (int k=1;k<=n;k++)      //先枚举中间节点         for (int i=1;i<=n;i++) if (i != k)            for (int j=1;j<=n;j++) if (j != k && j != i)                map[i][j] = min(map[i][j],map[i][k]+map[k][j]);    printf("%d\n",map[s][t]);    return 0;}
 

        那么,Floyed算法还有没有其它的用途呢?当然有,首先的一个作用便是判断负权回路,实现很简单,只需要在最后判断map[i][i](0<i<=n)是否小于0,若是,则存在负权回路。但这种判回路的方法时间复杂度为O(n^3),效率远远比不上BellmanFord,因此不经常用。

        除此之外,Floyed最小环算法被广泛地使用,如果你要解决这样一个问题:在一个有向图中,可从任意一顶点出发,求最小回路是多少?

        虽然本题可以使用Dijkstra来完成,但没有Floyed方便快捷。Floyed最小环算法在网上有介绍,这里不作详解,仅贴上核心代码

    //Floyed最小环     //map[i][j]表示原距离     for (int k=1;k<=n;k++)      //先枚举中间节点         for (int i=1;i<=n;i++) if (i != k)            for (int j=1;j<=n;j++) if (j != k && j != i)                ans = min(ans,f[i][j]+map[i][k]+map[k][j]);

Dijkstra算法:

        Dijkstra是以贪心为基础的算法,它的原理很好理解,每次都找与起点距离最小的点,再不断更新,直到所有点都被更新过为止。强烈建议使用邻接表来进行存储,因为这样既省空间又省时间,在vector的辅助下,存边已经不需要链表的辅助。

#include <cstdio>#include <cmath>#include <cstring>#include <vector>using namespace std;const int MAXN = 10001;const int INF = 1000001;struct Node{int to,dist;};int n,m,s,t;int dist[MAXN];bool vis[MAXN];vector <Node> G[MAXN];int main(){scanf("%d%d",&n,&m);for (int i=1;i<=m;i++){int from,to,dist;scanf("%d%d%d",&from,&to,&dist);G[from].push_back((Node){to,dist});}scanf("%d%d",&s,&t);memset(dist,127,sizeof(dist));for (int i=0;i<G[s].size();i++) dist[G[s][i].to] = min(dist[G[s][i].to],G[s][i].dist);dist[s] = 0; vis[s] = true;for (int i=1;i<=n;i++){        int k,_min = INF;;        for (int j=1;j<=n;j++) if (!vis[j] && dist[j] < _min)        {            k = j;            _min = dist[j];        }        if (_min == INF) break;        vis[k] = true;        for (int j=0;j<G[k].size();j++)            if (!vis[G[k][i].to] && dist[G[k][i].to] > dist[k]+G[k][i].dist) dist[G[k][i].to] = dist[k]+G[k][i].dist;}printf("%d\n",dist[t]);return 0;}

        从以上代码我们发现,Dijkstra算法的时间复杂度为O(n^2),而大部分时间都是用来查找最小边权。有什么数据结构可以来快速寻找最值呢?优先队列!实现方法较为简单,直接上代码

Heap Dijkstra:

#include <cstdio>#include <cmath>#include <cstring>#include <vector>#include <queue>using namespace std;const int MAXN = 10001;struct Node{int to,dist;bool operator < (const Node &x) const{        return dist > x.dist;    }};int n,m,s,t;int dist[MAXN];bool done[MAXN];vector <Node> G[MAXN];int main(){scanf("%d%d",&n,&m);for (int i=1;i<=m;i++){int from,to,dist;scanf("%d%d%d",&from,&to,&dist);G[from].push_back((Node){to,dist});}scanf("%d%d",&s,&t);memset(dist,127,sizeof(dist));    priority_queue <Node> Q;Q.push((Node){s,0});    dist[s] = 0;    while (!Q.empty())    {        Node x = Q.top();Q.pop();        if (!done[x.to])        {            done[x.to] = true;            for (int i=0;i<G[x.to].size();i++)                if (dist[G[x.to][i].to] > dist[x.to] + G[x.to][i].dist)                {                    dist[G[x.to][i].to] = dist[x.to] + G[x.to][i].dist;                    Q.push((Node){G[x.to][i].to,dist[G[x.to][i].to]});                }         }            }printf("%d\n",dist[t]);return 0;}
        从上面这段代码我们可以发现,优先队列维护的Dijkstra的时间复杂度为O(n*logn),是一个稳定的优秀算法。正因为它的稳定性,它的作用甚至比SPFA还大——除了不能处理负权。在许多比赛中,正因为SPFA有着不稳定性,常常会被出题人卡数据,导致SPFA降为O(VE)的算法,因此强烈建议在竞赛中使用Heap Dijkstra。

BellmanFord算法:

        在许多比赛中,边权常常有负值——甚至会出现负权回路。因此,最稳定的Heap Dijkstra便无法发挥出它的作用。于是,BellmanFord算法便展现出了它的威力。O(VE)的算法,可以很好的处理负权并有效地判断负权回路。

#include <cstdio>#include <cstring>#include <vector>using namespace std;const int MAXN=1001;struct Node{    int from,to,dist;};int n,m,s,t;int dist[MAXN];int cnt[MAXN];vector <Node> G;int main(){    scanf("%d%d",&n,&m);    for (int i=1;i<=m;i++)    {        int from,to,dist;        scanf("%d%d%d",&from,&to,&dist);        G.push_back((Node){from,to,dist});            }    scanf("%d%d",&s,&t);    memset(dist,127,sizeof(dist));    for (int i=0;i<m;i++) if (G[i].from == s) dist[G[i].to] = min(dist[G[i].to],G[i].dist);    dist[s] = 0;    for (int i=1;i<=n;i++)    {        bool flag = false;        for (int j=0;j<m;j++)        {            if (dist[G[j].to] > dist[G[j].from]+G[j].dist)            {                flag = true;                dist[G[j].to] = dist[G[j].from]+G[j].dist;            }        }        if (!flag) break;//避免冗余操作     }    bool Negative_Ctyle = false;    for (int i=0;i<m;i++) if (dist[G[i].to] > dist[G[i].from]+G[i].dist)    {        Negative_Ctyle = true;        break;    }    if (!Negative_Ctyle) printf("%d\n",dist[t]);else printf("Negetive Ctyle Exists!\n");    return 0;}  
        由代码我们可以发现,O(VE)的操作中存在许多冗余操作,即使是加了flag来进行判断,也仅仅是去除了少部分冗余,在效率上并没有太大的提升。因此,为了有效地提升效率,SPFA算法便产生了。它使用队列来维护松弛操作,将时间复杂度降为O(kE)(可以证明k的平均大小为2),代码如下

SPFA:

#include <cstdio>#include <cstring>#include <vector>#include <queue>#include <algorithm>using namespace std;const int MAXN=1001;const int INF=100000;struct Node{    int to,dist;};int n,m,s,t;int dist[MAXN];//离源点的最短距离 int cnt[MAXN]; //判断是否存在负权回路 bool inq[MAXN];//是否在队列中 vector <Node> G[MAXN];bool SPFA(int s){    for (int i=1;i<=n;i++) dist[i]=INF;    memset(inq,false,sizeof(inq));    dist[s]=0;inq[s]=true;    queue <int> Q;    Q.push(s);    while (!Q.empty())    {        int x=Q.front();Q.pop();        inq[x]=false;        for (int i=0;i<G[x].size();i++)            if (dist[x]+G[x][i].dist<dist[G[x][i].to])            {                dist[G[x][i].to]=dist[x]+G[x][i].dist;                if (!inq[G[x][i].to])                {                    Q.push(G[x][i].to);                    inq[G[x][i].to]=true;                    if (++cnt[G[x][i].to] > n) return true;                }            }    }     return false;}int main(){    scanf("%d%d",&n,&m);    for (int i=1;i<=m;i++)    {        int from,to,dist;        scanf("%d%d%d",&from,&to,&dist);        G[from].push_back((Node){to,dist});            }    scanf("%d%d",&s,&t);    bool Negative_Ctyle = SPFA(s);    if (!Negative_Ctyle) printf("%d\n",dist[t]);else printf("Negative Ctyle Exist!\n");    return 0;} 

        以上代码还可以很好地实现差分约束系统,这里不作详解。

        关于SPFA与BFS,这也是最近看书的时候看到的,其实很多BFS的题目都可以由SPFA解决,比如经典的迷宫问题,用SPFA效率会快很多。


后记:

        为了更好地帮助新手实现模板,这里给出最短路径的模板题目

        http://www.rqnoj.cn/problem/341        

        同时,推荐几道题,供大家学习。

        https://www.vijos.org/p/1119(标准最短路,没什么难度但很累)

        https://www.vijos.org/p/1046(Floyed最小环)

        http://poj.org/problem?id=1860(BellmanFord判断负权回路)

        https://www.vijos.org/p/1155(次小路)





0 0
原创粉丝点击