• 最短路径问题


    参考链接

    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算法可以大致分为三个部分

    1.初始化:将除源点外的所有顶点的最短距离估计值 dist[v] ← +∞, dist[s] ←0;
    2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
    3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 dist[v]中。

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

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

    伪代码:

    procedure BellmanFord(list vertices, list edges, vertex source)
       // 该实现读入边和节点的列表,并向两个数组(distance和predecessor)中写入最短路径信息
    
       // 步骤1:初始化图
       for each vertex v in vertices:
           if v is source then distance[v] := 0
           else distance[v] := infinity
           predecessor[v] := null
    
       // 步骤2:重复对每一条边进行松弛操作
       for i from 1 to size(vertices)-1:
           for each edge (u, v) with weight w in edges:
               if distance[u] + w < distance[v]:
                   distance[v] := distance[u] + w
                   predecessor[v] := u
    
       // 步骤3:检查负权环
       for each edge (u, v) with weight w in edges:
           if distance[u] + w < distance[v]:
               error "图包含了负权环"
        #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;  
        }  
    

      

    SPFA算法

    适用范围:

    SPFA算法是Bellman算法的改进版,它们是适用范围是相同的

    算法思想:

    利用队列动态更新最小值

    设dist[i]代表s到i点的当前最短距离,fa代表s到i的当前最短路径的前一个点的编号。开始时dist初始值无穷大,只有dist[s] = 0,fa全为0。

    维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点s,用一个布尔数组记录每个点是否在队列中。

    每次迭代,取出头节点v,依次枚举从v出发的边v->u,设边长度为len,如果dist[u] > dist[v]+len,则改进dist[u],将fa[u]记为v,并且由于s到u的最短距离变小了,有可能u可以改进其他的点,所以如果u不在队列里,就把它放进队尾。

    若一个点的最短路径被改进的次数达到n,则有负权环。可以通过SPFA算法判断图有无负权环。

    代码实例(邻接矩阵存图)

    void spfa(int s){
    	//dist[n]初始值无穷大
    	dist[s] = 0;
    	queue<int> q;
    	q.push(s);
    	vis[s] = true;//v在队列里
    	while(q.empty()){//队列不为空 
    		int v = q.front();
    		q.pop();
    		vis[v] = 0;//v已经不在队列中 
    		for(int u = 0;u < n;u++){
    			if(dist[u] > dist[v]+cost[v][u]){
    				dist[u] = dist[v] + cost[v][u];
    				fa[u] = v;
    				updataTimes[u]++; //更新了多少次 
    				if(vis[u] == 0)	q.push(u);
    			}
    		}
    	} 
    }
    

      

    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]);
    }
    

      

  • 相关阅读:
    梅小雨20191010-2 每周例行报告
    梅小雨20190919-1 每周例行报告
    梅小雨20190919-4 单元测试,结对
    王可非 20191128-1 总结
    20191121-1 每周例行报告
    20191114-1 每周例行报告
    对“都是为了生活”小组成员帮助的感谢
    20191107-1 每周例行报告
    20191031-1 每周例行报告
    20191024-1 每周例行报告
  • 原文地址:https://www.cnblogs.com/jaszzz/p/12853025.html
Copyright © 2020-2023  润新知