最短路径问题

Posted jaszzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最短路径问题相关的知识,希望对你有一定的参考价值。

参考链接

Dijkstra算法

算法特点:

迪科斯彻算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

算法的思路

Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

算法示例演示

下面我求下图,从顶点v1到其他各个顶点的最短路径

技术图片

 

 

首先第一步,我们先声明一个dis数组,该数组初始化的值为:
技术图片

我们的顶点集T的初始化为:T={v1}

既然是求 v1顶点到其余各个顶点的最短路程,那就先找一个离 1 号顶点最近的顶点。通过数组 dis 可知当前离v1顶点最近是 v3顶点。当选择了 2 号顶点后,dis[2](下标从0开始)的值就已经从“估计值”变为了“确定值”,即 v1顶点到 v3顶点的最短路程就是当前 dis[2]值。将V3加入到T中。
为什么呢?因为目前离 v1顶点最近的是 v3顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得 v1顶点到 v3顶点的路程进一步缩短了。因为 v1顶点到其它顶点的路程肯定没有 v1到 v3顶点短.

OK,既然确定了一个顶点的最短路径,下面我们就要根据这个新入的顶点V3会有出度,发现以v3 为弧尾的有: < v3,v4 >,那么我们看看路径:v1–v3–v4的长度是否比v1–v4短,其实这个已经是很明显的了,因为dis[3]代表的就是v1–v4的长度为无穷大,而v1–v3–v4的长度为:10+50=60,所以更新dis[3]的值,得到如下结果:
技术图片

因此 dis[3]要更新为 60。这个过程有个专业术语叫做“松弛”。即 v1顶点到 v4顶点的路程即 dis[3],通过 < v3,v4> 这条边松弛成功。这便是 Dijkstra 算法的主要思想:通过“边”来松弛v1顶点到其余各个顶点的路程。

然后,我们又从除dis[2]和dis[0]外的其他值中寻找最小值,发现dis[4]的值最小,通过之前是解释的原理,可以知道v1到v5的最短距离就是dis[4]的值,然后,我们把v5加入到集合T中,然后,考虑v5的出度是否会影响我们的数组dis的值,v5有两条出度:< v5,v4>和 < v5,v6>,然后我们发现:v1–v5–v4的长度为:50,而dis[3]的值为60,所以我们要更新dis[3]的值.另外,v1-v5-v6的长度为:90,而dis[5]为100,所以我们需要更新dis[5]的值。更新后的dis数组如下图:
技术图片

 

 

然后,继续从dis中选择未确定的顶点的值中选择一个最小的值,发现dis[3]的值是最小的,所以把v4加入到集合T中,此时集合T={v1,v3,v5,v4},然后,考虑v4的出度是否会影响我们的数组dis的值,v4有一条出度:< v4,v6>,然后我们发现:v1–v5–v4–v6的长度为:60,而dis[5]的值为90,所以我们要更新dis[5]的值,更新后的dis数组如下图:

技术图片

 

 

然后,我们使用同样原理,分别确定了v6和v2的最短路径,最后dis的数组的值如下:

技术图片

 

 

因此,从图中,我们可以发现v1-v2的值为:∞,代表没有路径从v1到达v2。所以我们得到的最后的结果为:

起点  终点    最短路径    长度
v1    v2     无          ∞    
      v3     {v1,v3}    10
      v4     {v1,v5,v4}  50
      v5     {v1,v5}    30
      v6     {v1,v5,v4,v6} 60

算法的代码实现

复杂度:O( |E| log|V| )

int cost[MAX_N][MAX_N];
int d[MAX_N];
bool used[MAX_N];
int v;
void dijkstra(int s)
{
	fill(d,d+V,INF);
	fill(used,used+V,false);
	d[s]=0;

	while(true)
	{
		int v=-1;
		for(int u=0;u<V;u++)
			if(!used[u]&&(v==-1||d[u]<d[v]))	v=u;
		
		if(v==-1)	break;
		used[v]=true;

		for(int u=0;u<V;u++)
			d[u]=min(d[u],d[v]+cost[v][u]);
	}
}

  

下面是使用STL 的 priority_queue 实现的,复杂度为O(|E|)

struct edge{int to,cost;};
// 指向顶点to的权为cost的边 typename pair<int,int>P;
// first:最短距离;second:顶点的编号 int V; vector<edge> G[MAX_N]; int d[MAX_N];  // 顶点s出发的最短距离 void dijkstra_2(int s){ // 堆按照first从小到大的取出顺序 priority_queue<P,vector<P>,greater<P>> que; fill(d,d+V,INF); d[s]=0; que.push(P(0,s)); while(!que.empty()){ P p=que.top(); que.pop(); int v=p.second; if(d[v]<p.first) continue; for(int i=0;i<G[v].size();i++){ edge e=G[v][i]; if(d[e.to]>d[v]+e.cost){ d[e.to]=d[v]+e.cost; que.push(P(d[e.to],e.to)); } } } }

  

Bellman-Ford算法

Dijkstra算法是处理单源最短路径的有效算法,但它局限于边的权值非负的情况,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的。

这时候,就需要使用其他的算法来求解最短路径,Bellman-Ford算法就是其中最常用的一个。该算法由美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。

适用条件&范围:

单源最短路径(从源点s到其它所有顶点v);

有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);

边权可正可负(如有负权回路输出错误提示);

差分约束系统;

算法的流程如下:

给定图G(V, E)(其中V、E分别为图G的顶点集与边集),源点s,数组Distant[i]记录从源点s到顶点i的路径长度,初始化数组Distant[n]为, Distant[s]为0;

以下操作循环执行至多n-1次,n为顶点数:
对于每一条边e(u, v),如果Distant[u] + w(u, v) < Distant[v],则另Distant[v] = Distant[u]+w(u, v)。w(u, v)为边e(u,v)的权值;
若上述操作没有对Distant进行更新,说明最短路径已经查找完毕,或者部分点不可达,跳出循环。否则执行下次循环;

为了检测图中是否存在负环路,即权值之和小于0的环路。对于每一条边e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的边,则图中存在负环路,即是说改图无法求出单源最短路径。否则数组Distant[n]中记录的就是源点s到各顶点的最短路径长度。

可知,Bellman-Ford算法寻找单源最短路径的时间复杂度为O(V*E).

Bellman-Ford算法可以大致分为三个部分

第一,初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算。
第三,遍历途中所有的边(edge(u,v)),判断是否存在这样情况:d(v) > d (u) + w(u,v) 则返回false,表示途中存在从源点可达的权为负的回路。

之所以需要第三部分的原因,是因为,如果存在从源点可达的权为负的回路。则 应为无法收敛而导致不能求出最短路径。 

有向图的Bellman-Ford算法代码:

    #include<iostream>  
    #include<cstdio>  
    using namespace std;  
      
    #define MAX 0x3f3f3f3f  
    #define N 1010  
    int nodenum, edgenum, original; //点,边,起点  
      
    typedef struct Edge //边  
    {  
        int u, v;  
        int cost;  
    }Edge;  
      
    Edge edge[N];  
    int dis[N], pre[N];  
      
    bool Bellman_Ford()  
    {  
        for(int i = 1; i <= nodenum; ++i) //初始化  
            dis[i] = (i == original ? 0 : MAX);  
        for(int i = 1; i <= nodenum - 1; ++i)  
            for(int j = 1; j <= edgenum; ++j)  
                if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)  
                {  
                    dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;  
                    pre[edge[j].v] = edge[j].u;  
                }  
                bool flag = 1; //判断是否含有负权回路  
                for(int i = 1; i <= edgenum; ++i)  
                    if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)  
                    {  
                        flag = 0;  
                        break;  
                    }  
                    return flag;  
    }  
      
    void print_path(int root) //打印最短路的路径(反向)  
    {  
        while(root != pre[root]) //前驱  
        {  
            printf("%d-->", root);  
            root = pre[root];  
        }  
        if(root == pre[root])  
            printf("%d
", root);  
    }  
      
    int main()  
    {  
        scanf("%d%d%d", &nodenum, &edgenum, &original);  
        pre[original] = original;  
        for(int i = 1; i <= edgenum; ++i)  
        {  
            scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);  
        }  
        if(Bellman_Ford())  
            for(int i = 1; i <= nodenum; ++i) //每个点最短路  
            {  
                printf("%d
", dis[i]);  
                printf("Path:");  
                print_path(i);  
            }  
        else  
            printf("have negative circle
");  
        return 0;  
    }  

  

Floyd算法

算法的特点:

弗洛伊德算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。

算法的思路

通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入两个矩阵,矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。矩阵P中的元素b[i][j],表示顶点i到顶点j经过了b[i][j]记录的值所表示的顶点。

假设图G中顶点个数为N,则需要对矩阵D和矩阵P进行N次更新。初始时,矩阵D中顶点a[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则a[i][j]=∞,矩阵P的值为顶点b[i][j]的j的值。 接下来开始,对矩阵D进行N次更新。第1次更新时,如果”a[i][j]的距离” > “a[i][0]+a[0][j]”(a[i][0]+a[0][j]表示”i与j之间经过第1个顶点的距离”),则更新a[i][j]为”a[i][0]+a[0][j]”,更新b[i][j]=b[i][0]。 同理,第k次更新时,如果”a[i][j]的距离” > “a[i][k-1]+a[k-1][j]”,则更新a[i][j]为”a[i][k-1]+a[k-1][j]”,b[i][j]=b[i][k-1]。更新N次之后,操作完成!

算法的实例过程

 技术图片

 

 

第一步,我们先初始化两个矩阵,得到下图两个矩阵:

技术图片技术图片

 

第二步,以v1为中阶,更新两个矩阵:
发现,a[1][0]+a[0][6] < a[1][6] 和a[6][0]+a[0][1] < a[6][1],所以我们只需要矩阵D和矩阵P,结果如下:

技术图片技术图片

 

通过矩阵P,我发现v2–v7的最短路径是:v2–v1–v7

第三步:以v2作为中介,来更新我们的两个矩阵,使用同样的原理,扫描整个矩阵,得到如下图的结果:

技术图片技术图片

 

OK,到这里我们也就应该明白Floyd算法是如何工作的了,他每次都会选择一个中介点,然后,遍历整个矩阵,查找需要更新的值,下面还剩下五步,就不继续演示下去了。

 代码:

复杂度O(|V|3)

 

int d[MAX_V][MAX_V]; // d[u][v]表示e=(u,v)的权值
int V;

void wallshall_floyd(){
	for(int k=0;k<V;k++)
		for(int i=0;i<V;i++)
			for(int j=0;j<V;j++)
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}

  

以上是关于最短路径问题的主要内容,如果未能解决你的问题,请参考以下文章

最短路径问题-Dijkstra(基于图的ADT)

最短路径

多源最短路径--Floyd-Warshall算法

地铁最短路径代码分析

(王道408考研数据结构)第六章图-第四节5:最短路径之弗洛伊德算法(思想代码演示答题规范)

最短路径问题 (最短路模板)