Part 0. 最短路是解决这类问题的,给定一个图 $ G = (V,E) $ ,以及图中各边的距离,询问两点间的最短路径是多少。常用的有以下几种算法.
Part 1. Dijkstra算法
能够计算一个节点到其他所有节点的最短路径.前提是 $ G $ 中的边的权值均不为负.
下面是我的代码习惯:
$ G[i][j] $ 表示节点 $ i $ 和节点 $ j $ 之间的距离
$ N $ 表示节点个数. $ st $ 表示起点. $ inf $ 表示一个很大的数 若 $ G[i][j] = inf $ 则表示 节点 $ i $ 和 $ j $ 不可达
$ low[i] $ 表示节点 $ i $ 到 $ st $ 的距离
松弛操作 $ Relax(u,v,w) if (low[v] > low[u] + w) low[v] = low[u] + w; $ 也就是判断$ st \rightarrow v $ 和 $ st $ $ \rightarrow $ $ u $ $ \rightarrow $ $ v $ 哪个路径更短.
集合 $ S $ 中的节点均为已确定最短路的节点,并用 $ vis[i] = true $ 来标记
$ path[i] $ 表示节点 $ i $ 在最短路径中的前驱
算法过程:
初始化操作
for each vertex v low[v] = G[st][v] ; path[v] = -1; low[st] = path[st] = 0 ;
求解步骤
- 刚开始时 $ S $ 中只有 $ st $,选择一个 $ S $ 外的节点 $ v $ ,且满足 $ low[v] <= low[u] \ \&\&\ u \in V - S $,即 $ v $ 是所有未确定最小值的点中距 $ st $ 最近的,那么可以直接确定 $ v $ 不能再被松弛,若可以被松弛则满足 $ low[v] > low[u] + G[u][v] \ \&\&\ u \in V-S $ 这与 $ low[v] <= low[u] \ \&\&\ u \in V-s $ 矛盾。所以,令 $ S[2] = v , vis[v] = true $ 。
- 现在集合 $ S $ 中已经有了两个元素了,那么刚刚加入的元素有什么用处么?我们尝试用它来松弛其他每一个 $ S $ 外的节点。
- 刚刚加入的节点已经起到了它的作用,现在只需要重复步骤 1 即可,也就是从 $ V - S $ 中寻找 $ low[v] <= low[u] $ ,同样,节点 $ V $ 可以直接加入集合 $ S $ ,同样用反证法证明
- 不断重复步骤 1 , 2 直到所有节点加入到 $ S $
代码如下:
1 class Dijkstra 2 { 3 public: 4 int N , R , st , G[maxn][maxn] , low[maxn] , path[maxn] ; 5 bool vis[maxn] ; 6 void Init(int n,int r,int sst) { 7 N = n , R = r , st = sst ; 8 memset(vis,false,sizeof(vis)) ; 9 memset(G,inf,sizeof(G)) ; 10 } 11 void addedge(int u,int v,int w) { 12 G[u][v] = G[v][u] = w ; 13 } 14 void DIJ() { 15 rep(i,N) { 16 G[i][i] = 0 ; 17 low[i] = G[st][i] ; 18 if (low[i] == inf) path[i] = -1 ; 19 else path[i] = st ; 20 } 21 path[st] = 0 ; 22 vis[st] = true ; 23 int tmp , id ; 24 rep(i,N-1) { 25 tmp = inf ; 26 rep(j,N) { 27 if (!vis[j] && low[j] < tmp) { 28 tmp = low[j] ; 29 id = j ; 30 } 31 } 32 vis[id] = true ; 33 rep(j,N) { 34 if (!vis[j] && low[j] > low[id] + G[id][j]) { 35 low[j] = low[id] + G[id][j] ; 36 path[j] = id ; 37 } 38 } 39 } 40 } 41 void pathprint(int en) { 42 int p = en ; 43 while (p != st) { 44 printf("%d ",p) ; 45 p = path[p] ; 46 } 47 printf("%d\n",st) ; 48 } 49 }ans;
Dijkstra 算法还有一种队列优化的形式,道理是一样的.代码在下面:
hdu 2544
1 #include <iostream> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <cstring> 5 #include <algorithm> 6 #include <vector> 7 #include <queue> 8 using namespace std ; 9 #define rep(i,n) for (int i = 1 ; i <= n ; ++ i) 10 #define LL long long 11 const int inf = 0x3f3f3f3f ; 12 const int maxn = 110 ; 13 struct EDGE 14 { 15 int u , v , w ; 16 }; 17 struct HEHE 18 { 19 int u , w ; 20 friend bool operator < (const HEHE & P,const HEHE & T) { 21 return T.w < P.w ; 22 } 23 }; 24 25 class DIJKSTRA 26 { 27 public: 28 vector<EDGE> egs ; 29 vector<int> G_id[maxn] ; 30 bool vis[maxn] ; 31 int low[maxn] , N , R , st , path[maxn] ; 32 void Init(int n,int r,int sst) { 33 N = n , R = r , st = sst ; 34 rep(i,N) { 35 G_id[i].clear() ; 36 vis[i] = false ; 37 low[i] = inf ; 38 path[i] = -1 ; 39 } 40 egs.clear() ; 41 low[st] = path[st] = 0 ; 42 } 43 void addedge(int u,int v,int w) { 44 egs.push_back(EDGE{u,v,w}) ; 45 G_id[u].push_back(egs.size()-1) ; 46 } 47 void DIJ() { 48 priority_queue<HEHE> Q ; 49 while (!Q.empty()) Q.pop() ; 50 HEHE tmp ; 51 int u ; 52 Q.push(HEHE{st,0}) ; 53 while (!Q.empty()) { 54 tmp = Q.top() ; 55 Q.pop() ; 56 u = tmp.u ; 57 for (int i = 0 ; i < G_id[u].size() ; ++ i) { 58 EDGE & e = egs[G_id[u][i]] ; 59 if (low[e.v] > low[u] + e.w) { 60 low[e.v] = low[u] + e.w ; 61 Q.push(HEHE{e.v,low[e.v]}) ; 62 path[e.v] = u ; 63 } 64 } 65 } 66 } 67 void pathprint(int en) { 68 int p = en ; 69 while (p != st) { 70 printf("%d ",p); 71 p = path[p] ; 72 } 73 printf("%d \n",st); 74 } 75 76 }ans; 77 78 int main() 79 { 80 int N , R , u , v , x ; 81 while (scanf("%d%d",&N,&R) == 2 && N && R) { 82 ans.Init(N,R,1) ; 83 rep(i,R) { 84 scanf("%d%d%d",&u,&v,&x) ; 85 ans.addedge(u,v,x) ; 86 ans.addedge(v,u,x) ; 87 } 88 ans.DIJ() ; 89 printf("%d\n",ans.low[N]) ; 90 // while (cin >> v) { 91 // ans.pathprint(v) ; 92 // } 93 } 94 return 0 ; 95 }
Part 2. Floyd算法
能够计算任意两点之间的最短路径,同样无法处理有负权边的情况.
算法过程
初始化操作 $ G[i][j] = inf $
求解步骤 三重循环
$ Floyd $ 算法是一种动态规划的思想 令 $ G[k][i][j] $ 表示 $ i \rightarrow j $ 的路径上经过的节点编号不大于 $ k $ 的最短距离,当 $ k = N $ 时即为最短路径。现在已知 $ G[k-1][i][j] $ 考虑怎么求解 $ G[k][i][j] $
若经过 $ k $ 节点,则 $ G[k][i][j] = G[k-1][i][k] + G[k-1][k][j] $
若不经过 $ k $ 节点,则 $ G[k][i][j] = G[k-1][i][j] $
而最终 $ G[k][i][j] $ 就要取其中较小值.
可以看出这是一个动态规划的转移方程,初始状态怎么确定呢,很简单, $ G[0][i][j] = distance[i][j] $
从转移方程可以看出 $ k $ 要放在最外层循环,否则会出错的,下面有一个样例
代码如下:
1 class FLOYD 2 { 3 public: 4 int N , R , G[maxn][maxn] , path[maxn][maxn] ; 5 void Init(int n,int r) { 6 N = n , R = r ; 7 memset(G,inf,sizeof(G)) ; 8 } 9 void addedge(int u,int v,int w) { 10 G[u][v] = G[v][u] = w ; 11 } 12 void floyd() { 13 for (int i = 1 ; i <= N ; ++ i) { 14 for (int j = 1 ; j <= N ; ++ j) { 15 if (G[i][j] == inf) path[i][j] = -1 ; 16 else path[i][j] = j ; 17 } 18 } 19 for (int k = 1 ; k <= N ; ++ k) { 20 for (int i = 1 ; i <= N ; ++ i) { 21 for (int j = 1 ; j <= N ; ++ j) { 22 if (G[i][j] > G[i][k] + G[k][j]) { 23 G[i][j] = G[i][k] + G[k][j] ; 24 path[i][j] = path[i][k] ; 25 } 26 } 27 } 28 } 29 } 30 void pathprint(int st,int en) { 31 // printf("now is solving %d and %d\n",st,en) ; 32 printf("%d ",st) ; 33 while (st != en) { 34 printf("%d ",path[st][en]) ; 35 st = path[st][en] ; 36 } 37 puts("") ; 38 } 39 }ans;
Part 3. Bellman_Ford算法
这个可以计算含负权边的图.若含负环则返回 $ false $
还能用于差分约束系统.
算法原理 最短路径一定不会含环(无论正环负环或者零),所以最短路径的最大长度是 $ |V| - 1 $ ,构造以 $ st $ 为树根的最短路径树,则树深不超过 $ |V| - 1 $。先看下代码:
for (int i = 1 ; i < N ; ++ i) { for (int j = 1 ; j <= R ; ++ j) { EDGE & e = edges[j] ; if (low[e.to] > low[e.fr] + e.di) { low[e.to] = low[e.fr] + e.di ; } } }//初始化low[i] 为无穷大,low[st] = 0
当内层循环循环了一遍之后,生成了树的第一层节点,至少能够找出所有与 $ st $ 直接相连的节点的最短路,
内层循环第二遍之后,会生成深度为二的节点,也就是和 $ st $ 之间间隔了一个节点的最短路径
依次执行下去就会得到最优解。
比如下图,首先初始化 $$ low[st] = 0 , low[A] = inf , low[B] = inf , low[C] = inf $$ 假设存储的边的顺序是 (st,A),(st,B),(B,A),(B,C),那么执行一次内层循环过程后就是这样的: $$ low[st] = 0 , low[A] = 10 , low[B] = 3 , low[A] = 8 , low[C] = 13 $$居然一下子求出了最短路,不过这实在是巧合,比如边的顺序是这样的话(B,C),(B,A),(st,B),(st,A),一次循环后是:$$ low[C] = inf , low[A] = inf , low[B] = 3 , low[A] = 10 $$ 这是最坏的情况,不过即使是在这样的情况下它也求得了 st 直接到各个点的距离,第二次之后是 $$ low[C] = 13 , low[A] = 8 , low[B] = 3 , low[A] = 8 $$ 这个时候得到的是 st 到各个节点的路径上有不多于一个点的情况下的最短路,第三次之后还是这样(因为已经不能松弛了)
1 #include <iostream> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <cstring> 5 #include <algorithm> 6 #include <cmath> 7 #include <vector> 8 using namespace std ; 9 #define rep(i,n) for (int i = 1 ; i <= n ; ++ i) 10 #define LL long long 11 const int maxn = 110 ; 12 struct Node 13 { 14 int u ; 15 int v ; 16 double r ; 17 double c ; 18 }; 19 20 class Bellman_Ford 21 { 22 public: 23 int N , R , st , cnt ; 24 double low[maxn] ; 25 Node egs[maxn<<1] ; 26 void Init(int n,int r,int sst,double my) { 27 N = n , R = r , st = sst ; 28 rep(i,N) low[i] = 0.0 ; 29 low[st] = my , cnt = 0 ; 30 } 31 void addedge(int u,int v,double r,double c) { 32 egs[++cnt] = Node{u,v,r,c} ; 33 } 34 bool bellmanford() { 35 bool ok ; 36 rep(i,N-1) { 37 ok = false ; 38 rep(j,cnt) { 39 Node & e = egs[j] ; 40 if ((low[e.u] - e.c)*e.r > low[e.v]) { 41 low[e.v] = (low[e.u] - e.c)*e.r ; 42 ok = true ; 43 } 44 } 45 if (!ok) break ; 46 } 47 rep(j,cnt) { 48 Node & e = egs[j] ; 49 if ((low[e.u] - e.c)*e.r > low[e.v]) { 50 return true ; 51 } 52 } 53 return false ; 54 } 55 }ans; 56 57 int main() 58 { 59 double c , r , w , my ; 60 int N , R , st , u , v; 61 // freopen("in.txt","r",stdin) ; 62 while (scanf("%d%d%d%lf",&N,&R,&st,&my) == 4) { 63 ans.Init(N,R,st,my) ; 64 rep(i,R) { 65 scanf("%d%d%lf%lf",&u,&v,&r,&c) ; 66 ans.addedge(u,v,r,c) ; 67 scanf("%lf%lf",&r,&c) ; 68 ans.addedge(v,u,r,c) ; 69 } 70 if (ans.bellmanford()) puts("YES") ; 71 else puts("NO") ; 72 } 73 return 0; 74 }
Part 4.spfa 算法.
求单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm,是西南交通大学段凡丁于1994年发表的。
---摘自百度百科
在刚才的 Bellman_Ford算法中,可以看到可能会做很多没必要的松弛尝试操作,比如,第一次边的顺序里,low[B] = inf 那么所有从 B 点出发的边都不用检查的,spfa算法就是把之前松弛过的节点放入队列,再用这些点尝试松弛其他节点(比如一个点没有松弛过,那从他出去的边也无法松弛其他节点),代码大致是这样的:
void spfa() { rep(i,N) { vis[i] = false , low[i] = inf ; } low[st] = 0 , vis[st] = true ; queue<int> Q ;
Q.push(st) ; //while (!Q.empty()) Q.pop() ; while (!Q.empty()) { u = Q.front() ; vis[u] = false ; Q.pop() ; for (int i = h[u] ; i != -1 ; i = G[i].nxt) { v = G[i].to , w = G[i].val ; if (low[v] > low[u] + w) { low[v] = low[u] + w ; if (!vis[v]) vis[v] = true , Q.push(v) ; } } } }
参考:
http://www.cnblogs.com/hxsyl/p/3270401.html
http://www.cnblogs.com/hxsyl/p/3248391.html#top
http://blog.csdn.net/zhongkeli/article/details/8832946
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html
http://blog.csdn.net/niushuai666/article/details/6772706
http://nopainnogain.iteye.com/blog/1047818
http://blog.csdn.net/xu3737284/article/details/8973615
end~_~