最短路径问题

  • Post author:
  • Post category:其他




最短路径问题



Dijkstra算法

(读者可以将其读作“迪杰斯特拉算法”)用来解决单源最短路问题,即给定图G和起点s,通过算法得到S到达其他每个顶点的最短距离。


Dijkstra 的基本思想是对图G(V,E)设置集合S,存放已被访问的顶点,然后每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短距离。这样的操作执行n次(n为顶点个数),直到集合S已包含所有顶点。

为了让算法的过程有更有画面感,下面将举一个形象的例子,希望读者能通过这个例子对Dijkstra算法的流程有较为清晰的把握,以降低编写代码的难度。

在世界的另一端,有一个美丽富饶的精灵大陆,那里存在着六个城市,城市中生存着许多小精灵。一开始,这个大陆和平安详,没有纷宇,但定月一人,心而咱万力心m文式x时治了这片大陆,并把大陆中的六个城市用黑暗力量染成了黑色,精灵们感到十分恐惧。这时,光明之神派遣一只被称为“番长狮子”(名字叫“亚历山大”,见图10-26)的英雄带领军队前往解救(“番长”是精灵大陆中一个很厉害的称号,只有实力非常强大的生灵才能拥有)。由于黑暗力量的影响,城市与城市之间只能通过某种特殊通道单向到达(即有向边),并且一些城市之间无法直接到达(即只能通过其他城巾间按到达)。图10-L0a 足一惘地图口石人城市个城市(编号为从V至Vs)和它们之间存在的有向边(边上的数字表示距离),且每个城市都被污染成了黑色(表示该城市暂时没有被解救)。亚历山大在研究地图后决定从V开始对六个城市的敌人发起进攻,且每成功攻下一个城市,就用光明之力让城市中的黑暗消散,并把带来的部队驻扎在城市中,以防黑暗势力攻回,自己则通过魔法重新回到V,带领新的部队攻打下一个城市。为了减少消耗,亚历山大希望每次从起点V到达需要攻占的城市之间的路程尽可能短,即从起点到达其他所有顶点都必须是最短距离。

在这里插入图片描述

此处省略很多,关于这个英雄的争战故事,差不多就是:

首先,Dijkstra算法解决的是单源最短路问题,即给定图G(VE)和起点s(起点又称为源点),求从起点s到达其它顶点的最短距离。


Dijkstra算法的策略

是:

设置集合S存放已被访问的顶点(即已攻占的城市),然后执行n次下面的两个步骤(n为顶点个数):

1每次从集合V-S(即未攻占的城市)中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S(即令其已被攻占)。

②之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点

v之间的最短距离。

Dijkstra 算法的具体实现:

由于Dijkstra算法的策略比较偏重于理论化,因此为了方便编写代码,需要想办法来实现策略中两个较为关键的东西,即集合S的实现、起点s到达顶点V(0≤i≤n-1)的最短距离的实现。

①集合S可以用一个 bool型数组vis[]来实现,即当vis[i]== true时表示顶点V;已被访问,当vis[i]== false时表示顶点V未被访问。

令int型数组d]表示起点s到达顶点V;的最短距离,初始时除了起点s的d[s]赋为0,其余顶点都赋为一个很大的数(初学者可以用1000000000,即 109;稍微懂点二进制编码的话可以使用十六进制0x3ff,但不要使用0x7ff,因为两个这样的数相加可能会超过int的表示范围)来表示inf,即不可达。

从复杂度来看,主要是外层循环O(V)与内层循环(寻找最小的d[u]需要O(V)、枚举v需要O(adj[u].size))产生的。又由于对整个程序来说,枚举v的次数总共为O(2唱) adj[u].size)=O(E),因此总复杂度为o(V2+E)。

可以注意到,上面的做法都是复杂度O(v’)级别的,其中由于必须把每个顶点都标记为已访问,因此外层循环的O(V)时间是无法避免的,但是寻找最小d[u]的过程却可以不必达到O(V)的复杂度,而可以使用堆优化来降低复杂度。最简洁的写法是直接使用STL 中的优先队列priority _queue,这样使用邻接表实现的 Dijkstra算法的时间复杂度可以降为O(VlogV+E)。此外,Dijkstra算法只能应对所有边权都是非负数的情况,如果边权出现负数,那么 Dijkstra算法很可能会出错,这时最好使用 SPFA 算法。


void Dijkstra(int s)//s 为 start起点
{
    memset(d,INF,sizeof(d));
    d[s] = 0;
    for(int i=0;i<n;i++)
    {
        int u=-1,MIN = INF;
        for(int j=0;j<n;j++)
        {
            if(vis[j]==false &&d[j]<MIN)
            {
                u = j;
                MIN = d[j]; //跟新节点距离
            }
        }
        if(u == -1) return ;
        vis[u] = true;
        for(int v = 0; v<n;v++)
        {
            if(vis[v]==false && G[u][v]!=INF && d[u]+G[u][v]<d[v]){
                d[v] = d[u]+G[u][v];
            }
        }
    }
}

Dijkstra算法可以很好地解决无负权图的最短路径问题,但如果出现了负权边,Dijkstra 算法就会失效。



Bellman-Ford算法

为了更好地求解有负权边的最短路径问题,需要使用Bellman-Ford算法(简称BF算法)。和Dijkstra算法一样,Bellman-Ford算法可解决单源最短路径问题,但也能处理有负权边的情况。Bellman-Ford 算法的思路简洁直接,易于读者掌握。

现在考虑环,也就是从某个顶点出发、经过若干个不同的顶点之后可以回到该顶点的情况。而根据环中边的边权之和的正负,可以将环分为零环、正环、负环(如图10-40所示,环A→B→C中的边权之和分别为0、正、负)。显然,图中的零环和正环不会影响最短路径的求解,因为零环和正环的存在不能使最短路径更短;而如果图中有负环,且从源点可以到达,那么就会影响最短路径的求解;但如果图中的负环无法从源点出发到达,则最短路径的求解不会受到影响。

与Dijkstra算法相同,Bellman-Ford算法设置一个数组d,用来存放从源点到达各个顶点的最短距离。同时 Bellman-Ford算法返回一个bool值:如果其中存在从源点可达的负环,那么函数将返回false;否则,函数将返回true,此时数组d中存放的值就是从源点到达各顶点的最短距离。

Bellman-Ford 算法的主要思路如下面的伪代码所示。需要对图中的边进行V-1轮操作,每轮都遍历图中的所有边:对每条边u→v,如果以u为中介点可以使d[v]更小,即 d[u]+length[u->v]<d[v]成立时,就用d[u] + length[u->v]更新d[v]。同时也可以看出,Bellman-Ford算法的时间复杂度是O(VE),其中n是顶点个数,E是边数。

此时,如果图中没有从源点可达的负环,那么数组d中的所有值都应当已经达到最优。

因此,如下面的伪代码所示,只而安丹对从H心风1方1i占可达的负环,返回false;否则,满足d[u]+ length[u->y]<d[v],如果有,则说明图中有从源点可达的负环,返回 false;否则,说明数组d中的所有值都已经达到最优,返回true。

那么,为什么 Bellman-Ford算法是正确的呢?想要了解完整数学证明的读者可以参考算法导论,下面给出一个简洁直观的证明。

首先,如果最短路径存在,那么最短路径上的顶点个数育足个公超过V个想一怨1么?)。于是,如果把源点s作为一棵树的根结点,把具他结息按照取短路位的以1就会生成一棵最短路径树。图10-41是最短路径树的一个例子。显然,麦R更题各全树Tz_公源点S到达其余各顶点的路径就是原图中对应的最短路径,且原图和源点一旦确定,最短路径树也就确定了。另外,由于最短路径上的顶点个数不超过V,因此最短路径树的层数一定不超过V。

用优先队列做也很不错:

#include<bits/stdc++.h>
//用途 :单源最短路
//优先队列弹出来的  原来在堆一定比后来弹出;来的大
using namespace std;
vector<vector<int> > d(100);
int dist[100];//标记
int n,m;
void bfs(int cur)
{
    queue <int> que;
    que.push(cur);
    while(!que.empty())
    {
        int temp=que.front();
        que.pop(); //把队首节点出队 
        if(dist[temp]!=0)  continue;
        cout<<temp<<endl;
        dist[temp]=1;
        for(int i=0;i<d[temp].size();i++)
        {
            if(!dist[d[temp][i]]) //if nor visited
            {
                que.push(d[temp][i]); //入队
            }
        }
    }
}
int main()
{
    cin >> n >> m;
    int x, y, w;
    for (int i = 0; i < m; ++i) {
    scanf("%d%d%d", &x, &y, &w);
    d[x].push_back(y);
    d[y].push_back(x);
    }
    for (int i = 0; i < n; ++i) {
        cout << i << " -> ";
        for (int j = 0; j < d[i].size(); ++j) cout << d[i][j] << " ";
        cout << endl;
    }
    memset(dist, 0, sizeof(dist));
    bfs(0);
    return 0;
    
}



Floyd算法

(读者可以将其读作“弗洛伊德算法”)用来解决全源s方的图 G(V,E),求任意两点u, v之间的最短路在长度,时间复杂度是O(n^3),管法是非常合适且方决定了顶点数n的限制约在200 以内,因此使用邻接矩阵来实现 Floyd算法是非常合适且方便的。

Floyd算法基于这样一个事实:如果存在顶点k,使得以k作为中介点时顶点i和顶点j的当前最短距离缩短,则使用顶点k作为顶点i和顶点j的中介点,即当dis[i][k] +dis[k][]< dis[i]0]时,令dis[i]] = dis[i][k]+ dis[k]们](其中 dis[i]i]表示从顶点i到顶点j的最短距离)。如图10-42所示,从V到V4的距离为3,而以V2为中介点时可以使V1到V4的距离缩短为2,那么就把V1到V4的距离从3优化为2,即当dis[1][2] + dis[2][4]<dis[1][4]时,令dis[1][4] = dis[1][2]+ dis[2][4]。

void Floyd(){
	for(int k=0;k<n; k++)
	{
		for(int i=0;i<n; i++)
			for(int j= 0;j<n; j++){
			if(dis[i][k] !- INF && dis[k][j] != INF && dis[i][k] +dis [k][j]< dis[i][j]){
			dis[i][j]=dis[i][k] +dis[kK][jl;//找到更短的路径
		}

对Floyd算法来说,需要注意的是:不能将最外层的k循环放到内层(即产生i->j->k的三重循环),这会导致最后结果出错。理由是:如果当较后访问的dis[u][v]有了优化之后,前面访问的dis[i]i会因为已经被访问而无法获得进一步优化(这里i、j先于u、v进行访问)。



版权声明:本文为qq_44627002原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。