最短路问题的三种基本算法(模板)

了解了优先队列,本来想写一道题目练练手,结果就看到了8441,看着像是bfs求最短路,然而T了,并不知道怎么优化,然后又去找老师要了标程,结果神仙代码看不懂(主要是因为太菜..),看到里面用了dijstra,就干脆先从最短路问题入手。

最短路问题,一般有三种方法,dijstra,bellman-forward,floyed,三者个有特色,适合于不同的场合。

一。dijstra(迪杰斯特拉)

 

 

 

Dijkstra算法

1.定义概览

Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。该算法无法处理负权边。

问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径。(单源最短路径)

 

2.算法描述

1)算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。

2)算法步骤:

a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。

b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。

c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。

d.重复步骤b和c直到所有顶点都包含在S中。

 

3.执行动画过程如下图

4.例题:http://icpc.upc.edu.cn/problem.php?id=2716

算法实现:

#include <iostream>
#include <bits/stdc++.h>
// dijstra n^2 TLE
using namespace std;
const int maxn=5e6+10;
const int inf=1e9+7;
struct E
{
    int v,w;
};
vector <E> edge[maxn];
int in[maxn],dis[maxn];//in 数组表示在集合S内,dis表示到个点的最短距离
int dijstra(int s,int e,int n)//s 出发点 e 终止点 n 点数
{
    for (int i=0; i<=n; i++)
        dis[i]=inf;
    dis[s]=0,in[s]=1;
    for (int i=0; i<edge[s].size();i++)
    {
        dis[edge[s][i].v]=edge[s][i].w;
        //printf("to%d=%d\n",edge[s][i].v,dis[edge[s][i].v]);
    }
    //初始化
    for (int i=0; i<=n; i++)
    {
        int mi=inf,k=s;//找到s点最短距离的点
        for (int j=1; j<=n; j++)
        {
            if (!in[j] && dis[j]<mi)
            {
                mi=dis[j];
                k=j;
            }
        }
        in[k]=1;//将最短的新点加入集合S
        int num=edge[k].size();//用新点k去扩展新点
        for (int j=0; j<num; j++)
        {
            int v=edge[k][j].v,w=edge[k][j].w;
            if (!in[v])
            {
                if (dis[k]+w<dis[v]) //relax
                {
                    dis[v]=dis[k]+w;
                }
            }
        }
    }
    return dis[e];
}
int main()
{
    int n,m,t;
    scanf("%d%d%d",&n,&m,&t);
    for (int i=1; i<=m; i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[u].push_back({v,w});
        edge[v].push_back({u,w});
    }
    int ans=dijstra(1,t,n);
    printf("%d\n",ans);
    return 0;
}

其实dj算法就是BFS+贪心,它每次选一个点,然后扩散(bfs)到它的邻点之后,再从所有点中,选出离起点最近的点,继续扩散出去。这样总共n-1次之后,图上所有点离起点的距离必然是最小的。时间复杂度为n^2,n>1000一般稳稳地TLE。

6.优化:

  考虑到每次都是用最近的那一个结点更新,暴力跑需要n的时间,太慢了。

  怎么样能gkd呢?我们自然可以想到优先队列,因为每次要找的点有鲜明的特征,是距离s最近的点。这样优化后,n变成了logn,所以总的复杂度变为nlogn,瞬间快乐。

代码实现:

#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e6+10;
const int inf=INT_MAX/2;
/*struct E
{
    int v,w;
    bool operator< (const E& b) const
    {
        return w > b.w;
    }
};*/
struct E
{
    int v,w;
    friend bool operator< (E x,E y)
    {
        return x.w>y.w;
    }
    //重载<运算符,使得距离小的优先级大
};
/*struct cmp
{
    bool operator() (const E &x,const E &y) const
    {
        return x.w>y.w;
    }
};*/
int vis[maxn],dis[maxn];
vector <E> edge[maxn];
int dijheap(int s,int e,int n)
{
    priority_queue <E> Q;
    for (int i=0; i<=n; i++)
        dis[i]=inf;
    Q.push({s,0});
    dis[s]=0;
    while(!Q.empty())
    {
        E cur=Q.top();//保证取出的队首元素就是距离s最近的
        Q.pop();
        int cv=cur.v;
        if (vis[cv]) continue;
        vis[cv]=1;
        int num=edge[cv].size();
        for (int i=0;i<num;i++)//用这个点去扩展relax
        {
            int v=edge[cv][i].v,w=edge[cv][i].w;
            if (!vis[v])
            {
                if (dis[v]>dis[cv]+w)
                {
                    dis[v]=dis[cv]+w;
                    Q.push({v,dis[v]});
                }
            }
        }
    }
    return dis[e];
}

int main()
{
    int n,m,t;
    scanf("%d%d%d",&n,&m,&t);
    for (int i=1; i<=m; i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[u].push_back({v,w});
        edge[v].push_back({u,w});
    }
    int ans=dijheap(1,t,n);
    printf("%d\n",ans);
    return 0;
}

7.小结

Dijstra算法十分优秀,在使用堆(优先队列)优化的情况下,时间复杂度为nlogn,如果题目中不是单源的最短路,那么可以每个点都作为起点跑一下dj算法,n^2logn。

缺点:

dj算法是无法处理负权边的!为什么呢,因为dj算法是贪心BFS,而BFS有一个特点,就是短视! 它只能看到与自己相邻的点的情况,但是对于远方,它就一脸蒙蔽了。如果有两种走法,一种是直接走边长5到达,一种是先走10,再走-20到达,显然,我们的dijstra算法会直接走第一种。

8.扩展

其实,dijstra算法,还可以输出最短路的路径,只需要用一个pre数组记录一下每个节点的前驱结点,递归输出就可以了。

代码实现:

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn=1e3+20;
const int inf=1e9+7;
int dis[maxn],vis[maxn],pre[maxn];
struct E
{
    int v,w;
    bool friend operator< (E x,E y)
    {
        return x.w>y.w;
    }
};
vector <E> edge[maxn];
void dij(int s,int n)
{
    priority_queue <E> Q;
    while(Q.size()) Q.pop();
    for (int i=1; i<=n; i++)
        dis[i]=inf;
    dis[s]=0;pre[s]=s;
    Q.push({s,0});
    while(!Q.empty())
    {
        E cur=Q.top();
        Q.pop();
        int cv=cur.v;
        int num=edge[cv].size();
        if (vis[cv]) continue;
        vis[cv]=1;
        for (int i=0; i<num; i++)
        {
            int v=edge[cv][i].v,w=edge[cv][i].w;
            if (dis[v]>dis[cv]+w)
            {
                dis[v]=dis[cv]+w;
                Q.push({v,dis[v]});
                pre[v]=cv;
            }
        }
    }
}
void outway(int i)
{
    if (pre[i]!=i)
    {
        printf("%d-->",i);
        outway(pre[i]);
    }
    else printf("1\n");
    return ;
}
int main()
{
    int n,m;
    freopen("out2.txt","w",stdout);
    while(~scanf("%d%d",&n,&m))
    {
        if (n==0&&m==0) break;
        memset(vis,0,sizeof(vis));
        memset(edge,0,sizeof(edge));
        memset(pre,0,sizeof(pre));
        for (int i=1; i<=m; i++)
        {
            int u,v,w,flag=0;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].push_back({v,w});
        }
        dij(1,n);
        for (int i=2; i<=n; i++)
            i==n ? printf("%d\n",dis[i]) :printf("%d ",dis[i]);
        for (int i=2; i<=n; i++)
            outway(i);
    }
    return 0;
}

需要正序输出的话,其实也可以,用stack记录一下路径即可

代码如下:

void callway(int i,int x)
{
    while(pre[i]!=i)
    {
        way[x].push(i);
        i=pre[i];
    }
    way[x].push(1);
}
void outway(int x)
{
   while(way[x].size())
   {
       int a=way[x].top();
       way[x].pop();
       if (way[x].size()==0)
            printf("%d\n",a);
       else
        printf("%d-->",a);
   }
}

for (int i=2; i<=n; i++)
            callway(i,i),outway(i);

 

 

二。Bellman-Ford 算法

 

1.定义概览

Bellman - ford算法是求含负权图的单源最短路径的一种算法,效率较低(nm),代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。

问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径,边长可能为负值。(单源最短路径)

2.算法描述

每一条边松弛n次,对于任意一条最短路,最多松弛n-1次,如果还能松弛,说明存在负环

 

3.算法步骤

 

a.初始化

b.对每个点所连的边松弛

3.松弛检查负环

 

4.例题 http://icpc.upc.edu.cn/problem.php?id=1634

代码如下:

 

#include <iostream>
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e4+10;
const ll inf=2147483647;
struct E
{
    int v,w;
};
vector<E> edge[maxn];
ll dis[maxn];
bool bellman(int s,int n)
{
    for (int i=1; i<=n; i++)
        dis[i]=inf;
    dis[s]=0;
    for (int k=1; k<=n;k++)
    for (int i=1; i<=n; i++)
    {
        for (int j=0; j<edge[i].size();j++)
        {
            ll v=edge[i][j].v,w=edge[i][j].w;
            if (dis[v]>dis[i]+w)
                dis[v]=dis[i]+w;
        }
    }
    for (int i=1; i<=n; i++)
    {
        for (int j=0; j<edge[i].size();j++)
        {
            int v=edge[i][j].v,w=edge[i][j].w;
            if (dis[v]>dis[i]+w)
                return 1;
        }
    }
    return 0;
}
int main()
{
    int n,m,s;
    scanf("%d%d%d",&n,&m,&s);
    for (int i=1; i<=m; i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[u].push_back({v,w});
    }
    int flag=bellman(s,n);
    for (int i=1; i<=n-1; i++)
    {
        printf("%lld ",dis[i]);
    }
    printf("%lld\n",dis[n]);
   flag ? cout<<"Yes\n" : cout<<"No\n";
return 0; }

5.优化(SPFA)

朴素的bellman-ford算法时间复杂度是n*m,很容易超时

很多时候我们并不需要那么多次松弛,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛。

那么我们就用队列维护<那些结点可能会引起松弛>,就可以至访问必要的边了。

优化以后时间复杂度会是k*m,k为常数且很小。

代码如下:

#include <iostream>
#include <bits/stdc++.h>
 
using namespace std;
struct E
{
    int v,w;
};
const int maxn=2e4+10,inf=1e9+7;
vector <E> edge[maxn];
int in[maxn],dis[maxn];
void SPFA(int s,int n)
{
    for (int i=1;i<=n;i++) dis[i]=inf;
    dis[s]=0;
    queue <int> Q;
    Q.push(s);
    in[s]=1;
    while(!Q.empty())
    {
        int cur=Q.front();
        Q.pop();
        in[cur]=0;
        int num=edge[cur].size();
        for (int i=0; i<num; i++)
        {
            int v=edge[cur][i].v,w=edge[cur][i].w;
            if (dis[v]>dis[cur]+w)
            {
                dis[v]=dis[cur]+w;
                if (!in[v])
                    Q.push(v),in[v]=1;
            }
        }
    }
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i=1; i<=m; i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[u].push_back({v,w});
    }
    SPFA(1,n);
    for (int i=2;i<=n;i++)
    {
        printf("%d\n",dis[i]);
    }
    return 0;
}

 

6.小结

bellman-ford算法比较好写,但是时间复杂度不够好,即使是优化过的SPFA算法,也很容易被精心设计的稠密图给卡掉,毕竟理论上界还是n*m。

但是对于负权边或者判断负环,我们就必须使用bellman*ford算法了。

7.扩展

关于玄学复杂度的玄学优化

 SLF优化

SLF叫做Small Label First 策略。

比较当前点和队首元素,如果小于队首,则插入队首,否则加入队尾。

具体为啥可以优化,其实也是玄学,甚至对于有的数据,优化还会变慢。。。其实SPFA被卡的话,我觉得优化也没有意义的,所以只能算是锦上添花吧,不是很必要掌握(万一哪天水过了呢

代码如下:

#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn=5000010;
const int inf=1e9+7;
struct E
{
    int v,w;
};
vector <E> edge[maxn];
int in[maxn],dis[maxn];
int SPFA(int s,int e,int n)
{
    for (int i=1; i<=n; i++)
        dis[i]=inf;
    dis[s]=0;
    deque <int> Q;
    Q.push_back(s);
    in[s]=1;
    while(!Q.empty())
    {
        int cur=Q.front();
        Q.pop_front();
        in[cur]=0;
        int num=edge[cur].size();
        for (int i=0; i<num;i++)
        {
            int v=edge[cur][i].v,w=edge[cur][i].w;
            if (dis[v]>dis[cur]+w)
            {
                dis[v]=dis[cur]+w;
                if (!in[v])
                {
                    if (dis[v]<=dis[cur]) Q.push_front(v);
                    else Q.push_back(v);
                    in[v]=1;
                }
            }
        }
    }
    return dis[e];
}
int main()
{
    int n,m,t;
    scanf("%d%d%d",&n,&m,&t);
    for (int i=1; i<=m; i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[u].push_back({v,w});
        edge[v].push_back({u,w});
    }
    int ans=SPFA(1,t,n);
    printf("%d\n",ans);
    return 0;
}

三。Floyed算法(弗洛伊德算法)

 

1.定义概览

一种可以求出任意两点之间最短路的算法,支持正负权,可以实现传递闭包,时间复杂度N^3

2.算法描述

 

1)算法思想原理:

     Floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点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的最短路径的距离。

2).算法描述:

a.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。   

b.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

 

3.代码实现

memset(dis,0x3f,sizeof(dis));
for (int i=1; i<=n; i++)
    dis[i][i]=0;
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]);

4.扩展

对于有向图,有时我们只关心两点之间是否有通路,可以用0/1表示。然后吧循环语句改成 a[i][j]|=(a[i][k]&&a[k][j]);就可以啦

例题和代码:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=4124

#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int  maxn=105;
int a[maxn][maxn],ma[maxn],mi[maxn];
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        memset(a,0,sizeof(a));
        memset(ma,0,sizeof(ma));
        memset(mi,0,sizeof(mi));
        int n,m,flag=0;
        scanf("%d %d",&n,&m);
        for (int i=1; i<=m; i++)
        {
            int u,v;
            scanf("%d %d",&u,&v);
            if (u==v)  flag=1;
            a[u][v]=1;
        }
        for (int k=1; k<=n; k++)
        {
            for (int i=1; i<=n; i++)
            {
                for (int j=1; j<=n; j++)
                {
                    a[i][j]=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]&&a[j][i])
                {
                    flag=1;
                    break;
                }
            }
            if (flag) break;
        }
        if (flag)
        {
            for (int i=1; i<=n; i++)
                putchar(48);
            putchar(10);
            continue ;
        }
        for (int i=1; i<=n;i++)
        {
            for (int j=1; j<=n; j++)
            {
                if (a[i][j])
                    ma[i]++,mi[j]++;
            }
        }
        for (int i=1; i<=n; i++)
        {
            if (ma[i]<=n/2 && mi[i]<=n/2)
                putchar(49);
            else putchar(48);
        }
        putchar(10);
    }
    return 0;
}

四。总结

 

最短路算法是图论里最基础的算法,根据不同的情况,我们要有合适的选择。

对于负权边和判断负环,一般用SPFA;

对于需要输出路径的,一般用dijstra;

对于判断联通的,一般用floyd;

当题目没有特别强调有负权边时,一般应该选择dijstra,因为spfa很容易被人卡时间。除非数据很水。

 

posted @ 2019-05-22 15:54  Fos、伤感  阅读(782)  评论(0编辑  收藏  举报