最短路小结
在图的问题当中, 很多都是最短路的问题, 甚至有一些不等式的问题,也可以转换为最短路来进行解决。
在现如今, 在短路可以进行大致的分为两类, 一种是单源最短路, 另外一种就是任意两点间的最短路; 一个是求一个点到所有点的最短距离, 另外一个是求图中所有点相互到达的最短距离!
首先在介绍最短路解决办法之前, 有必要先介绍一下加权图的存储方法, 而我们的算法也自然会根据存储形式的不同在细节上面, 有着些许的变化!
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 */
注意事项:
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 }
进阶---》》由我们推倒的最短路的不等式进行出发, 来解决不等式的问题,(满足不等式的最大值) 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 }