在学习图论的过程中,最短论问题是比较常见且又具有代表性的一类问题。最短路是给定两个定点,在以这两个点作为起点和终点的路径中,边的权值和最小的路径。在实际生活中,最常见的最短路问题,就是在地图导航上应用。比如我们把权值作为距离,那么我们就可以求得A到B的最短路径。如果时间作为权值,那么我们就可以得到A到B的最短时间。
单源最短路问题就是将起点固定,求该起点到其他所有点的最短路问题。贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特(Lester Ford) 创立的,求解单源最短路径问题的一种算法。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行|V|-1次松弛操作,得到所有可能的最短路径。其优于Dijkstra算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。
设dist[v]表示从源点s到v的最短路径长度。对于与v相邻的任意顶点u,dist[v]满足三角不等式:
dist[v] ≤ dist[u] + w(u, v), (其中w(u,v)为边(u, v)的权值)
我们设d[v]为s到v的最短路权值上界(可能为无穷大,即不连通),称为最短路估计。
如果 d[v] > d[u] + w(u, v),即说明d[v]还可以变得更小。于是我们就使 d[v] = d[u] + w(u, v),我们称这个操作为松弛操作。
显然每次通过松弛操作我们都可以使得d[v]减小,直到d[v]的值不在变化(即当d[v]等于dist[v])。
我们容易知道,在一个没有负权环的图中。每一个顶点至多与其它|V|-1个顶点进行松弛操作,若大于|V|-1,则必然存在负权环。
对于图G(V,E),下面给出Bellman-Ford的算法流程:
输入:图G和起点S 输出:s到每一个点的最短路径,以及图中是否存在负权环 具体流程: 1、初始化d数组,d[s] = 0, d[i] = ∞ (i ≠ s) 2、枚举每一条边,进行松弛操作 3、将操作2重复执行|V|-1次 4、枚举每一条边,看是否能够进行松弛操作,若能,这说明原图存在负权环
对于Bellman-Ford,由于每次操作需要枚举|E|条边,总共需要重复|V|-1次操作,则我们容易得出其时间复杂度为O(VE)。如果我们使用队列进行优化,则时间复杂度可下降为O(kE),k是个比较小的系数(并且在绝大多数的图中,k<=2,然而在一些精心构造的图中可能会上升到很高)。
#include <cstdio> #include <cstring> #define INF 0xfffffff #define MAXN (100 + 10) using namespace std; struct edge{ int from, to; edge(int f = 0, int t = 0) : from(f), to(t){} }; edge es[MAXN*MAXN]; int cost[MAXN]; bool graph[MAXN][MAXN]; int d[MAXN]; //判断图是否联通 void Floyd(int n){ for(int i = 1; i <= n; i++){ for(int k = 1; k <= n; k++){ for(int j = 1; j <= n; j++){ if(!graph[i][j]) graph[i][j] = graph[i][k] && graph[k][j]; } } } } bool bellman_ford(int s, int V, int E){ for(int i = 0; i <= V; i++) d[i] = -INF; d[s] = 100; //重复对每一条边进行松弛操作 for(int k = 0; k < V-1; k++){ for(int i = 0; i < E; i++){ edge e = es[i]; //松弛操作 if(d[e.to] < d[e.from] + cost[e.to] && d[e.from] + cost[e.to] > 0){ d[e.to] = d[e.from] + cost[e.to]; } } } //检查负权环 for(int i = 0; i < E; i++){ edge e = es[i]; if(d[e.to] < d[e.from] + cost[e.to] && graph[e.to][V] && d[e.from] + cost[e.to] > 0) return true; } return d[V] > 0; } int main(){ int n, m, cnt, vex; while(scanf("%d", &n), n != -1){ memset(graph, false, sizeof(graph)); cnt = 0; for(int i = 1; i <= n; i++){ scanf("%d%d", &cost[i], &m); for(int j = 0; j < m; j++){ scanf("%d", &vex); es[cnt++] = edge(i, vex); graph[i][vex] = true; } } Floyd(n); if(!graph[1][n] || !bellman_ford(1, n, cnt)){ printf("hopeless\n"); } else{ printf("winnable\n"); } } return 0; }
单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm。 SPFA算法是西南交通大学段凡丁于1994年发表的。松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免了冗余计算。我们还是以hdu1317xyzzy为例,代码如下:
#include <cstdio> #include <cstring> #include <queue> #define MAXN (100 + 10) using namespace std; //d表示s到各点的所经过路径的权值之和 //cost表示各点的权值 //cnt表示进入队列的次数 int d[MAXN], cost[MAXN], cnt[MAXN]; //reach表示两点之间是否联通,即可达 //graph记录两点之间是否有边 bool reach[MAXN][MAXN], graph[MAXN][MAXN]; void Init(){ memset(d, 0, sizeof(d)); memset(cnt, 0, sizeof(cnt)); memset(graph, false, sizeof(graph)); memset(reach, false, sizeof(reach)); } //判断图是否联通 void Floyd(int n){ for(int i = 1; i <= n; i++){ for(int k = 1; k <= n; k++){ for(int j = 1; j <= n; j++){ if(!reach[i][j]) reach[i][j] = reach[i][k] && reach[k][j]; } } } } bool SPFA(int s, int n){ queue<int> Q; d[s] = 100; Q.push(s); while(!Q.empty()){ int now = Q.front(); Q.pop(); cnt[now]++; //如果不存在负权环(PS:在本题中为正权环),即每个点进入队列的次数至多为n-1 //若大于n-1,即表明必然存在负权环 if(cnt[now] >= n) return reach[now][n]; //依次枚举每条边 for(int next = 1; next <= n; next++){ if(graph[now][next] && d[now] + cost[next] > d[next] && d[now] + cost[next] > 0){ Q.push(next); d[next] = d[now] + cost[next]; } } } return d[n] > 0; } int main(){ int n, m, vex; while(scanf("%d", &n), n != -1){ Init(); for(int i = 1; i <= n; i++){ scanf("%d%d", &cost[i], &m); for(int j = 0; j < m; j++){ scanf("%d", &vex); reach[i][vex] = true; graph[i][vex] = true; } } Floyd(n); if(!reach[1][n] || !SPFA(1, n)){ printf("hopeless\n"); } else{ printf("winnable\n"); } } return 0; }
我们容易发现,如果图中没有负边的情况。在Bellman-Ford算法中,如果d[u]还不是最短距离的话,那么即便我们进行了松弛操作,那么d[v]也不会变为最短距离。而且即便d[v]没有变化,那么他还是需要检查一次所有的边。显然这些操作很浪费时间,于是乎,我们就提出了以下改进:
(1)从最短距离已经确定的顶点出发更新与之相邻顶点的最短距离。
(2)对于最短距离已经确定的顶点,我们直接无视。
通过这样的修改我们就得到了Dijkstra算法。Dijkstra算法是用来解决只含非负权值边的图的单源最短路问题。换而言之,Dijkstra无法处理含有负权边的图。
对于图G(V,E),下面给出Dijkstra的算法流程:
输入:图G和起点S 输出:s到每一个点的最短路径 具体流程: 1、初始化d数组,d[s] = 0, d[i] = ∞ (i ≠ s) 2、设置所有点未访问过(即设置一个标记数组,并将其置空) 3、找出所有未访问过的点中距离值最小的点,将其标记为访问过 4、对3中找到的点的相邻边进行松弛操作 5、重复3和4直到所有点都访问过
因为操作更新|V|次,每次操作需要找最小值,扫描一个点连接的所有边,如果我们使用堆来实现寻找和维护,则时间复杂度为O( (|E|+|V|) log|V| )。若只用普通的方法扫描,时间复杂度为O(|V|² + |E|)。
我们以hdu 1874畅通工程续 来说明Dijkstra算法:
#include <cstdio> #include <vector> #include <algorithm> #define MAXN 200 + 10 #define INF 0xffffff using namespace std; struct Vex{ int v, weight; Vex(int tv, int tw):v(tv), weight(tw){} }; //graph用来记录图的信息 vector<Vex> graph[MAXN]; //判断是否已经找到最短路 bool inTree[MAXN]; //源点s到各顶点最短路的值 int mindist[MAXN]; //初始化 void Init(int n){ for(int i = 0; i < n; i++){ inTree[i] = false; graph[i].clear(); mindist[i] = INF; } } //s表示源点,t表示终点,n表示顶点数目 int Dijkstra(int s, int t, int n){ int tempMin, tempVex, addNode; //初始化s mindist[s] = 0; //将源点s标记为访问过 inTree[s] = true; //题目中可能有重边,我们去除重边 for(unsigned int i = 0; i < graph[s].size(); i++) mindist[graph[s][i].v] = min(mindist[graph[s][i].v], graph[s][i].weight); //从剩下的n-1个点逐个枚举 for(int nNode = 1; nNode <= n-1; nNode++){ tempMin = INF; //寻找所有未访问过点中,有最小距离的点 for(int i = 0; i < n; i++){ if(!inTree[i] && mindist[i] < tempMin){ tempMin = mindist[i]; addNode = i; } } //将该点标记为访问过 inTree[addNode] = true; //将与该点相邻的点进行松弛操作 for(unsigned int i = 0; i < graph[addNode].size(); i++){ tempVex = graph[addNode][i].v; if(!inTree[tempVex] && tempMin + graph[addNode][i].weight < mindist[tempVex]){ mindist[tempVex] = tempMin + graph[addNode][i].weight; } } } return mindist[t]; } int main(){ int n, m; int v1, v2, x, s, t; while(scanf("%d%d", &n, &m) != EOF){ Init(n); for(int i = 0; i < m; i++){ scanf("%d%d%d", &v1, &v2, &x); graph[v1].push_back(Vex(v2, x)); graph[v2].push_back(Vex(v1, x)); } scanf("%d%d", &s, &t); int ans = Dijkstra(s, t, n); if(ans == INF) printf("-1\n"); else printf("%d\n", ans); } return 0; }
#include <cstdio> #include <vector> #include <queue> #include <algorithm> #define MAXN 200 + 10 #define INF 0xffffff using namespace std; struct Vex{ int v, weight; bool operator < (const Vex & t) const{ return this->weight > t.weight; } Vex(int tv = 0, int tw = 0):v(tv), weight(tw){} }; vector<Vex> graph[MAXN]; bool inTree[MAXN]; int mindist[MAXN]; void Init(int n){ for(int i = 0; i < n; i++){ inTree[i] = false; graph[i].clear(); mindist[i] = INF; } } int Dijkstra_heap(int s, int t, int n){ priority_queue<Vex> Q; Vex tempVex; int v1, v2, weight; //初始化源点s的信息 mindist[s] = 0; Q.push(Vex(s, 0)); while(!Q.empty()){ //每次从堆中取出最小值 tempVex = Q.top(); Q.pop(); v1= tempVex.v; if(inTree[v1]) continue; //如果没有访问过,则我们将其标记为访问过 inTree[v1] = true; //将与其相邻的点,进行松弛操作 for(unsigned int i = 0; i < graph[v1].size(); i++){ v2 = graph[v1][i].v; weight = graph[v1][i].weight; if(!inTree[v2] && mindist[v1] + weight < mindist[v2]){ mindist[v2] = mindist[v1] + weight; //将满足条件的点重新加入堆中 Q.push(Vex(v2, mindist[v2])); } } } return mindist[t]; } int main() { int n, m; int v1, v2, x, s, t; while(scanf("%d%d", &n, &m) != EOF){ Init(n); for(int i = 0; i < m; i++){ scanf("%d%d%d", &v1, &v2, &x); graph[v1].push_back(Vex(v2, x)); graph[v2].push_back(Vex(v1, x)); } scanf("%d%d", &s, &t); int ans = Dijkstra_heap(s, t, n); if(ans == INF) printf("-1\n"); else printf("%d\n", ans); } return 0; }
PPS:在附赠一个大礼包,图论500题