最短路问题

来源:互联网 发布:a类网络ip地址 编辑:程序博客网 时间:2024/06/13 12:47

1.Dijkstra算法

适用类型

该算法同时适用于有向图和无向图,但仅适用于边权为正的情况,适用于求单源最短路径。

概述

Dijkstra算法是由E.W.Dijkstra提出的一种按照路径长度递增的次序分别产生到各顶点最短路径的贪心算法。

基本思路

  • 把带权图中的所有顶点分成两个集合S和V-S。集合S中存放已找到最短路径的顶点,V-S中存放当前还未找到最短路径的顶点。算法将按照最短路径长度递增的顺序逐个将V-S集合中的元素加入到S集合中,直至所有顶点都进入到S集合为止。
  • 其实,这里用到一个很重要的定理:下一条最短路径或者是弧(v0,vi),或者是中间经过S集合中的某个顶点,而后到达vi的路径。

    反证法证明:假设下一条最短路径上有一个顶点vj不在S集合中,即此路径为(v0,…,vj,…,vi)。显然,(v0,…,vj)的长度小于(v0,…,vj,…,vi)的长度,故下一条最短路径应为(v0,…,vj),这与题设的下一条最短路(v0,…,vj,…,vi)相矛盾。所以,下一条最短路径上不可能有不在S中的顶点vj。

  • 其中第一条最短路径是从源点v0到各点路径长度集合中长度最短者,在这条路径上,必定只有一条弧,并且这条弧的权值最小(设为v0->vk);下一条路径长度次短的最短路径只可能有两种情况:或者是直接从源点到该带你vi,或者是从源点经过已求得最短路径的顶点vk,再到达vi。再下一条路径长度次短的最短路径也有两种情况:或者是直接从源点到该点,或者是从源点经过顶点vk或vi,再到达该顶点,以此类推。

代码

//d[i]表示起点0到节点i的路径长度,v[i]标记是否已找出到节点i的最短路径 int i,x,minconst int INF=1<<30;memset(v,0,sizeof(v));for(i=0;i<n;i++){    d[i]=(i==0?0:INF);  } for(i=0;i<n;i++){    min=INF;    for(y=0;y<n;y++)    {        if(!v[y]&&d[y]<=min)        {            min=d[x=y];        }    }    v[x]=1;    for(y=0;y<n;y++)    {        d[y]=getMin(d[y],w[x][y]+d[x]);//邻接矩阵存放各条边    }}

除了求出最短路的长度之外,使用该算法也能很方便地打印出结点0到所有结点的最短路本身,只需要在更新d数组时维护”父亲指针“,这称为松弛操作,具体来说,需要把d[y]=getMin(d[y],w[x][y]+d[x])改成

if(d[y]>d[x]+w[x][y]){    d[y]=d[x]+w[x][y];    fa[y]=x;} 

优化

不难看出,上面程序的时间复杂度是O(n2),由于最短路算法实在是太重要了,下面我们把它优化到O(mlogn)。我们知道,在最坏的情况下,m和n2是同阶的,mlogn的复杂度要比n2高,但在很多情况下,图中的边并没有那么多,mlogn比n2小得多,m远小于n2的图称为稀疏图,用优化后的代码效率更高,而m相对较大的图成为稠密图,用上面的代码效率更高。

//把Dijkstra算法封装到一个结构体中 const int INF=1<<30;struct Edge            //边{    int from,to,dist;    Edge(int u,int v,int d):from(u),to(v),dist(d){} };struct HeapNode        //优先队列中的元素{    int d,u;//将顶点到源点的距离和顶点捆绑在一起    bool operator < (const HeapNode &hn)const    {        return d>hn.d;    }};struct Dijkstra{    int n,m;    vector<Edge> edges;       //保存每条边的具体信息     vector<int> G[maxn];      //保存以i为起点的各边的编号     bool done[maxn];          //是否已标号     int d[maxn];              //源点S到各个点的距离     int p[maxn];              //最短路中的上一条弧的编号     void init(int n)    {        this->n=n;        for(int i=0;i<n;i++)        {            G[i].clear();        }        edges.clear();    }    void addEdge(int from,int to,int dist)    {        edges.push_back(Edge(from,to,dist));        m=edges.size();        G[from].push_back(m-1);    }    void dijkstra(int s)    {        priority_queue<HeapNode> pq;        for(int i=0;i<n;i++)        {            d[i]=INF;        }        d[s]=0;        memset(done,0,sizeof(done));        pq.push((HeapNode){0,s});        while(!pq.empty())        {            HeapNode x=pq.top();            pq.pop();            int u=x.u;            if(done[u]) continue;            done[u]=true;            for(int i=0;i<G[u].size();i++)            {                Edge &e=edges[G[u][i]];                if(d[e.to]<d[u]+e.dist)                {                    d[e.to]=d[u]+e.dist;                    p[e.to]=G[u][i];                    pq.push((HeapNode){d[e.to],e.to});                }            }        }    }};

在松弛成功后,需要修改节点e.to的优先级,但STL中的优先队列不提供修改优先级的操作。因此,只能将新元素重新插入优先队列,这样做并不会影响结果的正确性,因为d值小的节点自然会先出队。

2.Floyd算法

适用类型

该算法适用于每对顶点之间的最短路径问题。

概述

Floyd算法是解决每对顶点之间的最短路径问题的比较直接的算法,属于典型的动态规划算法,形式比较简单。

基本思路

  • 首先设置一个矩阵F,用于记录路径长度。初始时,顶点vi到vj的最短路径长度F[i][j]=w[i][j],即弧(vi,vj)上的权值。若不存在弧(vi,vj),则F[i][j]=INF。此时,把矩阵记为F0,下面需要进行n次试探。
  • 让路径经过顶点v0,并比较路径(vi,vj)与路径(vi,v0,vj)的长度,去其中较短者作为最短路径的长度,把此时得到的矩阵记为F1,称F1为路径上的顶点序号不大于1的最短路径长度。
  • 在F1的基础上让路径经过顶点v1,并依据上一布的方法求得最短路径长度,得到F2,称F2为路径上的顶点序号不大于2的最短路径长度,以此类推。
  • 经过n次试探后,就把n个顶点都考虑在路径中了,此时求得的Fn就是各顶点之间的最短路径长度。实际上,该算法,可以记为如下的递推公式。

    F0[i][j]=w[i][j]
    Fk[i][j]=min{Fk-1[i][j],Fk-1[i][k-1]+Fk-1[k-1][j]} 0<=i,j,k<=n-1

代码

int i,j,k;for(k=0;k<n;k++){    for(i=0;i<n;i++)    {        for(j=0;j<n;j++)            {            d[i][j]=getMin(d[i][j],d[i][k]+d[k][j]);        }    }}

在调用它之前只需对矩阵做简单的初始化,除初始化有边相连的两点对应的值之外,还应该将d[i][i]赋为0,其他的d值都为INF。其中,INF的取值应当注意,为防止它的值过大而溢出,或过小而与实际存在的路的长度混淆,可以估计一下实际最短路长度的上限,并把INF设置成只比它大一点点的值。
在有向图中,有时不必关心路径的长度,而只关心两点之间是否有通路,则可以分别用1和0表示有通路和无通路,同时代码如下。这样的结果称为传递闭包。

int i,j,k;for(k=0;k<n;k++){    for(i=0;i<n;i++)    {        for(j=0;j<n;j++)            {            d[i][j]=d[i][j]||(d[i][k]&&d[k][j]);         }    }}

3.Bellman-ford算法

适用类型

Dijkstra算法仅适用于边权为正的情况,而该算法可用于求解边权为负的单源最短路径问题。

概述

Bellman-ford算法是由动态规划的提出者理查德•贝尔曼和小莱斯特•福特提出的解决带有负权的单源最短路径的算法。

基本思路

  • 首先明确一个问题:如果最短路存在,一定存在一个不含环的最短路,理由如下。

    在边权可正可负的图中,环有正环、负环和零环三种,如果包含零环或正环,去掉以后路径不会边长;如果包含负环,则意味着最短路不存在,因为环上的点的最短路可以无限变小。

  • 将除源点外的所有顶点的最短距离初值赋为无穷大,将源点的最短距离初值赋为0。

  • 反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离,此过程执行n-1次(因为最短路最多只经过不含起点的n-1个结点)。
  • 判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则表示s可达负环;否则s不可达负环,并且从源点可达的顶点v的最短距离保存在 d[v]中

代码

int i,j,x,y;for(i=0;i<n;i++){    d[i]=INF;   }d[0]=0;for(i=0;i<n-1;i++){    for(j=0;j<m;j++)    {        x=u[j];        y=v[j];        if(d[x]<INF&&d[y]>d[x]+w[j])        {            d[y]=d[x]+w[j];        }       }   }

优化

不难看出,上述代码的时间复杂度使O(mn),在实践中,常常用FIFO队列来代替上面的循环检查。

bool Bellman_ford(int s){    queue<int> qu;    memset(inq,false,sizeof(inq));    memset(cnt,0,sizeof(cnt));      for(i=0;i<n;i++)    {        d[i]=INF;       }    d[s]=0;    qu.push(s);    inq[s]=true;    while(!qu.empty())    {        u=qu.front();        qu.pop();        inq[u]=false;        for(i=0;i<G[u].size();i++)        {            Edge& e=edges[G[u][i]];            v=e.to;            if(d[u]<INF&&d[v]>d[u]+e.wight)            {                d[v]=d[u]+e.wight;                p[v]=G[u][i];                if(!inq[v])                {                    qu.push(v);                    inq[v]=true;                    cnt[v]++;                    if(cnt[v]>n)                    {                        return false;                       }                }            }        }    }    return true;}

采取FIFO队列的Bellman-Ford算法在最坏情况下需要O(mn)的时间,不过在实践中,往往只需要很短的时间就能求出最短路。上面的代码还有一个功能,在发现负圈时及时退出。注意,这只说明s可以到达一个负圈,并不代表s到每个点的最短路都不存在。另外,如果图中有其他负圈但是s无法到达这个负圈,则上面的算法也无法找到。

原创粉丝点击