最短路小结

  在图的问题当中, 很多都是最短路的问题, 甚至有一些不等式的问题,也可以转换为最短路来进行解决。

  在现如今, 在短路可以进行大致的分为两类, 一种是单源最短路, 另外一种就是任意两点间的最短路; 一个是求一个点到所有点的最短距离, 另外一个是求图中所有点相互到达的最短距离!

  

  首先在介绍最短路解决办法之前, 有必要先介绍一下加权图的存储方法, 而我们的算法也自然会根据存储形式的不同在细节上面, 有着些许的变化!

  1 .邻接矩阵存储。

     所谓邻接矩阵的存储就是,利用一个数组 eg: cost [i] [j] : 进行存储节点之间的权值, 根据是有向图还是无向图进行小小的判定! 注意点 初始化

  2 .邻接表存储。

     邻接表就是类似于稀疏矩阵, 相比较于邻接矩阵他更节省了空间, 而且因为和节点直接相关联, 也恰恰更方便于调用! 使用了  C ++ STL 中的 vector 更为方便。 但是他, 他需要遍历寻找点与点之间的是否有着边权

  3 .直接进行边的存储。

     这个就是直接进行了定义结构体,使用结构体存储了起始点, 边权以及结束点, 这个存储方法主要是由边出发, 对边进行遍历;

  

这几种图的表示方法各有优缺点, 邻接矩阵使用起来更为方便,但是他所需要的内存空间多, 而且在使用之前, 你还必须要记得进行初始化, 将那些没有边权的节点区分开,初始值可以任意, 只要是可以区分那些没有边权的节点。  使用邻接表大大节省了空间, 而且实现了和节点相关联, 还是不错的;  至于单单对边进行存储, 这种方法一定程度上脱离了节点, 但是在对于最小生成树可以这种纯边计算的时候有着应用。 

 

  1. 单源最短路的解决办法:

     a.  Bellman-Ford 算法 :(由边进行出发, 不断地更新点与点之间的距离, 最终更新完毕, 得到了最优解)——》要求:不能存在负圈;

     这个算法的主要思想就是使用等式:

      d[i] = min{ d[j] + cost [j] [i] };  然后巧妙使用边,不断地更新, 一直到不能更新, 最终找到了那个 min !!  

     算法缺点 : 存在负圈的情况下, 他会不断地经过那一个负圈, 不断的更新自己, 所以说不适用!

    复杂度 :    O (V E) 

     b.     Dijkstra 算法 : 这是算法是对 Bellaman-Ford 算法的有代价的简化, 减少了复杂度, 但是要求不能存在负边权;(这种算法是由点出发, 不断对点之间最短距离进行更新,直至不能更新);

     Dijkstra 算法是对Bellman-Ford 算法的改进,由于 Bellman-Ford 对边进行了最短距离的更新, 但是没有达到我们想要的效果, 就是尽可能以最少的步数达到等式  d[i] = min{ d[j] + cost [j] [i] }; 当中的最小值, 基于这一点,我们进行了优化:    

      • 对于找到最短距离的点,以他为中心, 更新和他相连的点
      • 对于那些最短距离已经确定的点不做处理

       这样就形成了 Dijkstra 算法, 这个算法实现由需要找刚刚确定最短距离的点, 然后以他为中心, 向相邻的点进行扩散, 找到最近的点。 基于他的思想, 我们可以轻易地看出来, 这个算法是以点为中心的, 因为他的优化是相对于点进行处理的, 那么我们需要使用邻接矩阵或者是邻接表进行存储。

      但是基于对刚刚确定最短距离点的寻找, 这个算法的设置, 进一步影响了你的运行效率;

      主要有着4种 

      邻接矩阵+普通记忆寻找,  邻接表+普通记忆寻找

        前两种复杂度 :   O(V2) ;    O(V E) ;

      邻接矩阵+优先队列寻找   邻接表+优先队列寻找

         优先队列实现的复杂度: O ()  ;   O (E log(V))  ;

      但是因为 Dijkstra 更新的是两点之间的 

  2.任意两点间最短路的解决办法:

      c.   Floyd-Warshall算法 :这是相当于使用dp 的一个算法 

       他的证明过程主要是通过不断地DP 进行缩小范围

      dp[k+1] [i] [j]  数组的含义是 在仅仅使用  0 - k 节点 和 i, j  节点的情况下, 节点 i 到达节点 j 的最短距离!    而且注意  dp [0] [i] [j] = cost[i] [j] , 所以说,我们在输入的时候,直接进行dp数组的赋值即可以!

      那么我们进行分情况讨论, 进而完成递归的过程:

      在不经过节点 k 的情况下:

      dp[k+1] [i] [j] = dp[k] [i] [j]

      经过节点 k 的情况下 :

      dp[k+1] [i] [j] = dp[k] [i] [k] + dp[k] [k] [j];

      那么合起来的话就是    

       dp[k+1] [i] [j] = min { dp[k] [i] [j],  dp[k] [i] [k] + dp[k] [k] [j] };

 

      此外该 dp 进行到了这一步, 虽然不能进行效率的简化, 但是内存的节省还是可以进行的

      本问题的dp 类似于背包问题的以为数组解决, 也可以仅仅使用一个二维数组, 进行不断地更新

      那么就写成了  dp[i] [j] = min { dp [i] [j], dp[i] [k], dp[k][j]};    总而言之, 他就是过k点, 和不过 k 节点的讨论, 在讨论中不断地更新, 最后更新完毕,得到那个结果!

      复杂度有点高 O(V3)

 

 

 

 

 

  附Bellman_Ford , Dijkstra  Floyd-Warshall 算法的代码 :

 

  1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstdlib>
  4 #include <cstring>
  5 #include <string>
  6 #include <vector>
  7 #include <queue>
  8 #include <list>
  9 #include <stack>
 10 #include <set>
 11 #include <cmath>
 12 #include <cctype>
 13 #include <algorithm>
 14 #include <map>
 15 using namespace std;
 16 
 17 typedef pair<int, int> P;
 18 typedef long long ll;
 19 struct Edge{
 20     int from, to, cost;     //起始节点, 终止节点, 边权;
 21 };
 22 struct node{
 23     int to, cost;           //邻接表所需要的结构体
 24 };
 25 
 26 const int MAX_V = 100;      // vertex : 顶点;
 27 const int MAX_E = 100;      // edge   : 边;
 28 const int INF = 1e8;
 29 vector<node> G[MAX_V];       //邻接矩阵;
 30 int cost[MAX_V][MAX_V];     //邻接矩阵;
 31 int d[MAX_V];               //最短距离更新数组;存储的是单源点到其他各点直接的距离
 32 int used[MAX_V];            //标记是否使用数组;记录的是最短距离已经是到达的顶点
 33 int V, E;                   //记录的顶点个数以及边的个数
 34 int pre[MAX_V];             //路径还原用来记录前导顶点的数组
 35 Edge edge[MAX_E];           //单单存储边
 36 int dp[MAX_V][MAX_V];
 37 
 38 void Show(void){
 39     for(int i = 0; i < V; i++){
 40         printf("d[%d] : %d\n", i, d[i]);
 41     }
 42 }
 43 //这个是对边进行的算法, 脱离的点
 44 void Bellman_Ford(int s){
 45     //初始化操作:
 46     fill(d, d + V, INF);    d[s] = 0;    //初始化最短距离数组
 47     while(true){
 48         bool used = false;
 49         for(int i = 0; i < 2 * E; i++){     //
 50             Edge ee = edge[i];
 51             if(d[ee.from] + ee.cost < d[ee.to]){
 52                 d[ee.to] = d[ee.from] + ee.cost;
 53                 used = true;
 54             }
 55         }
 56         if(!used)   break;
 57     }
 58 }
 59  //笨方法寻找刚刚确定的最短路, 邻接表
 60 void Dijkstra_clumsy1(int s){
 61     fill(d, d + V, INF);    d[s] = 0;
 62     fill(used, used + V, false);
 63     while(true){
 64         int v = -1;
 65         for(int u = 0; u < V; u++){     //寻找刚刚确定的最短路
 66             if(!used[u]&&(v == -1 || d[v] > d[u])){
 67                 v = u;                  //这个时候千万别更新used 数组, 因为这是时候只是一个局部最优解
 68             }
 69         }
 70         if(v == -1) break;              //已经是更新完毕,跳出循环的条件
 71         used[v] = true;
 72         //邻接表的写法
 73         for(int i = 0; i < G[v].size(); i++){
 74             node nn = G[v][i];
 75             if(d[nn.to] > nn.cost + d[v]){
 76                 d[nn.to] = nn.cost + d[v];
 77             }
 78         }
 79     }
 80 }
 81 //笨方法寻找刚刚确定的最短路, 邻接矩阵
 82 void Dijkstra_clumsy2(int s){
 83     fill(d, d + V, INF);    d[s] = 0;
 84     fill(used, used + V, false);
 85     while(true){
 86         int v = -1;
 87         for(int u = 0; u < V; u++){     //寻找刚刚确定的最短路
 88             if(!used[u]&&(v == -1 || d[v] > d[u])){
 89                 v = u;                  //这个时候千万别更新used 数组, 因为这是时候只是一个局部最优解
 90             }
 91         }
 92         if(v == -1) break;              //已经是更新完毕,跳出循环的条件
 93         used[v] = true;
 94         //邻接矩阵的写法
 95         for(int i = 0; i < V; i++){
 96             if(d[i] > d[v] + cost[v][i]){
 97                 d[i] = d[v] + cost[v][i];
 98             }
 99         }
100     }
101 }
102 //巧妙使用优先队列来进行寻找, 大大减少了算法的复杂度 ; 邻接矩阵
103 void Dijkstra_pque1(int s){
104     fill(d, d + V, INF);    d[s] = 0;
105     priority_queue<P, vector<P>, greater<P> > pque;
106     pque.push(P(0, s));
107     while(!pque.empty()){
108         P p = pque.top();   pque.pop();
109         if(p.first > d[p.second])   continue;
110         //邻接矩阵
111         for(int i = 0; i < V; i++){
112             if(d[i] > d[p.second] + cost[p.second][i]){
113                 d[i] = d[p.second] + cost[p.second][i];
114                 pque.push(P(d[i], i));
115             }
116         }
117     }
118 }
119 //巧妙使用优先队列来进行寻找, 大大减少了算法的复杂度 ; 邻接表
120 void Dijkstra_pque2(int s){
121     fill(d, d + V, INF);    d[s] = 0;
122     priority_queue<P, vector<P>, greater<P> > pque;
123     pque.push(P(0, s));
124     while(!pque.empty()){
125         P p = pque.top();   pque.pop();
126         if(p.first > d[p.second])   continue;
127         //邻接表
128         for(int i = 0; i < G[p.second].size(); i++){
129             node nn = G[p.second][i];
130             if(d[nn.to] > nn.cost + d[p.second]){
131                 d[nn.to] = nn.cost + d[p.second];
132                 pque.push(P(d[nn.to], nn.to));
133             }
134         }
135     }
136 }
137 //Floyd-Warshall
138 void Floyd_Warshall(){
139     for(int k = 0; k < V; k++){
140         for(int i = 0; i < V; i++){
141             for(int j = 0; j < V; j++){
142                 dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
143             }
144         }
145     }
146     for(int i = 0; i < V; i++){
147         printf("d[%d] : %d\n", i, dp[0][i]);
148     }
149 }
150 
151 int main()
152 {
153     cin>>V>>E;  //无向图
154     // 输入图的内容
155     for(int i = 0; i < V; i++){
156         for(int j = 0; j < V; j++)
157             cost[i][j] = dp[i][j] = INF;    //Fordy-Warshall 的记忆数组
158     }
159     for(int i = 0; i < E; i++){
160         int a, b, c;    scanf("%d%d%d", &a, &b, &c);
161         cost[a][b] = cost[b][a] = c;    //邻接矩阵
162         dp[a][b] = dp[b][a] = c;
163         node n; n.cost = c, n.to = b;   //邻接表
164         G[a].push_back(n);  n.to = a;
165         G[b].push_back(n);
166         edge[2*i].from = a, edge[2*i].to = b, edge[2*i].cost = c;
167         edge[2*i+1].from = b, edge[2*i+1].to = a, edge[2*i+1].cost = c; //一定要注意边的数量的变化
168     }
169     Bellman_Ford(0);
170     printf("Bellman_Ford  %d  \n", d[6]);   Show();
171     Dijkstra_clumsy1(0);
172     printf("Dijkstra_clumsy1  %d  \n", d[6]);   Show();
173     Dijkstra_clumsy2(0);
174     printf("Dijkstra_clumsy2  %d  \n", d[6]);   Show();
175     Dijkstra_pque1(0);
176     printf("Dijkstra_pque1  %d  \n", d[6]); Show();
177     Dijkstra_pque2(0);
178     printf("Dijkstra_pque2  %d  \n", d[6]); Show();
179     printf("Floyd_Warshall :  \n");
180     Floyd_Warshall();
181 
182     return 0;
183 }
184 
185 
186 /*
187 
188 7 10
189 0 2 5
190 0 1 2
191 1 2 4
192 1 3 6
193 1 4 10
194 2 3 2
195 3 5 1
196 4 5 3
197 4 6 5
198 5 6 9
199 
200 */
View Code

 

    

  注意事项: 

  1。 当你在做加权图的相关问题时, 一定要注意是有向图还是无向图;  因为这会对你接收数据以及存储的方式有着关键性的影响, 如果是有向图 那么 cost [i] [j] = a; 但是对于无向图就应该是 cost [i] [j] = cost [j] [i] = a;  意思是这个全是 i--> j 和 j --> i 这连个方向所共有的! 同样对于邻接矩阵也需要双向存储!

  2。一定要注意初始化, 比如说 存储距离的数组, 你的记录使用的记忆数组,记录最短路径的数组pre[i] 初始化 以及你存储边权的数组(如果有必要的话, 一定要记得初始化!!),而且记得初始化的位置放对!

  3。 邻接表和直接边存储的结构体一定要注意区分,虽然成员不会写错, 但常常会因为结构体名称记混, 而带来一个不必要的 debug 内容! vector<node> G[MAX_V]; 

  4. 注意图中的节点名称是从 0 开始, 还是从 1 开始 ! 这个节点名称会对你的存储操作有着些许的影响

  5. 对于无向图的边存储来说, 你的边的数量扩大了二倍, 所以说遍历边的时候, 注意他是 2 * E !

  6. 对于Bellman_Ford 以及 Fordy-warshall 算法 都是一点存在负圈就会失效,这一点对于简化算法的 Dijkstra 算法来说更为严格, 因为他即使是存在负边也不行! 

    7. 路径还原仅仅只是在更新的时候使用 pre[u] = v; 记忆前导数组, 不断地更新, 最后形成该节点真正的前导!至于路径的显示, 我们使用vector数组, 进行存储, 然后不断地添加进去, 最后使用一个 reverse 点到回来,即可;但是要记得初始化, 因为这个初始化数值需要用来判断是不是到了最后一个点;

 

 

 

       进阶---》》掌握思想,在原最短路的基础上面, 设计次短路的算法!!                  POJ  Num : 3255.

  次短路只是在原本最短路的基础之上, 加一个次短路的维护代码, d2 的存在是关键, 然后pque 不仅仅维护最短路, 也在维护着次短路数组

 

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstdlib>
 4 #include <cstring>
 5 #include <string>
 6 #include <cctype>
 7 #include <cmath>
 8 #include <vector>
 9 #include <queue>
10 #include <list>
11 #include <map>
12 #include <stack>
13 #include <set>
14 #include <algorithm>
15 using namespace std;
16 typedef pair<int, int> P;           //前面是存储距离,后面是存储点的编号
17 struct Node{
18     int to, cost;
19 };
20 const int MAX_V = 5050;
21 const int MAX_E = 101010;
22 const int INF = 1e8;
23 int V, E;
24 vector<Node> G[MAX_V];
25 int d[MAX_V], d2[MAX_V];
26 //注意本题目的点编号是从1 开始, 而且还是无向图。
27 int main()
28 {
29     cin>>V>>E;
30     int a, b, c;
31     for(int i = 0; i < E; i++){
32         scanf("%d%d%d", &a, &b, &c);
33         a--, b--;
34         Node n; n.to = b, n.cost = c;   G[a].push_back(n);
35         n.to = a, n.cost = c;   G[b].push_back(n);
36     }
37     fill(d, d + V, INF);  fill(d2, d2 + V, INF);
38     d[0] = 0;
39     priority_queue<P, vector<P>, greater<P> >   pque;
40     pque.push(P(0, 0));
41     while(!pque.empty()){
42         P p = pque.top();   pque.pop();
43         if(p.first > d2[p.second])   continue;
44         for(int i = 0; i < G[p.second].size(); i++){
45             Node n = G[p.second][i];
46             if(d[n.to] > p.first + n.cost){
47                 d2[n.to] = d[n.to];
48                 d[n.to] = p.first + n.cost;
49                 pque.push(P(d[n.to], n.to));
50             }
51             else if(d2[n.to] > p.first + n.cost){
52                 d2[n.to] = p.first + n.cost;
53                 pque.push(P(d2[n.to], n.to));
54             }
55         }
56     }
57     printf("%d\n", d2[V-1]);
58     return 0;
59 }
次短路 Code

 

     进阶---》》由我们推倒的最短路的不等式进行出发, 来解决不等式的问题,(满足不等式的最大值)    POJ : Num: 3169.

   这里有一条 v -》 u 的边,边权是w, 那么定会满足不等式 d(v) + W >= d(u); 所以说在我们的图当中, 有很多这样的不等式, 图中的最短距离都要满足, 而且是满足所有不等式的最大值, 为什么是最大值呢? 因为在所有的不等式, 一定存在不等式是基于最短路径满足的不等式, 等式左右两端相等, 那么一但右面的数值变大, 那么不等式不满足,因此是最大值!这是从不等式性质来分析, 我们通过图的性质也可以得出, 我们的距离不会是最小值, 因为右端不等式是可以取到0的, 然而我们的距离是通过路走出来的, 因此他不会是“0” 这个最小值, 换句话说, 他必须要加上边权, 经过一些路。

   所以说, 最短路问题, 可以看作是某些特殊不等式限制条件下, 求解最大值的问题!! 就像是 POJ 3169

  附代码:

 1 #include <iostream>
 2 #include <vector>
 3 #include <cstdio>
 4 #include <cstring>
 5 #include <string>
 6 #include <cstdlib>
 7 #include <cmath>
 8 #include <cctype>
 9 #include <algorithm>
10 #include <queue>
11 #include <list>
12 #include <map>
13 #include <stack>
14 #include <set>
15 using namespace std;
16 
17 struct Edge{
18     int from, to, cost;
19 };
20 const int INF = 1e8;
21 const int MAX_E = 20020;
22 const int MAX_V = 1010;
23 int EML, EMD, V, E;
24 int d[MAX_V];
25 Edge edge[MAX_E];
26 
27 //牛的标号是 1 -- N:
28 int main()
29 {
30     cin>>V>>EML>>EMD;   int a, b, c;    int E = 0;
31     for(int i = 0; i < EML; i++){
32         scanf("%d%d%d", &a, &b, &c);
33         edge[E].from = a - 1;   edge[E].to = b - 1; edge[E].cost = c;   E++;
34     }
35     for(int i = 0; i < EMD; i++){
36         scanf("%d%d%d", &a, &b, &c);
37         edge[E].from = b - 1;   edge[E].to = a - 1; edge[E].cost = -c;  E++;
38     }
39     fill(d, d + V, INF);    d[0] = 0;   bool used  = false, flag = false;
40     for(int i = 0; ; i++){
41         if(i == V + 1){
42             flag = true;   break;          //存在负圈, 直接完蛋
43         }
44         used  = false;
45         for(int j = 0; j < E; j++){
46             Edge ee = edge[j];
47             if(d[ee.to] > d[ee.from] + ee.cost){
48                 d[ee.to] = d[ee.from] + ee.cost;
49                 used = true;
50             }
51         }
52         if(!used)   break;              //没有可以继续更新的了, 直接跳出
53     }
54     if(flag)    printf("-1\n");
55     else{
56         if(d[V-1] == INF)   printf("-2\n");
57         else    printf("%d\n", d[V-1]);
58     }
59     return 0;
60 }

 

posted @ 2019-08-22 15:39  lucky_light  阅读(211)  评论(0编辑  收藏  举报