图论_最短路(更新中)
[最短路问题]
定义:起点固定,终点固定,求解两点间最短路径的问题,称为最短路问题。
分类:最短路问题分为单源最短路与多源最短路
[单源最短路]
1.Bellman-Ford算法
[前导知识]
(1)最短距离:d[i]表示从起点s出发到顶点i的最短距离。
满足 d[i] = min{d[j] + w[i][j]} j ∈ V
(2)DAG(Directed Acyclic Graph)即没有圈的有向图。
(3)拓扑排序:对于DAG,可以跑一遍拓扑排序得到该图顶点的拓扑序。
(4)圈:起点和终点重合的路径,称为圈。其中负圈表示跑一圈,边权值和为负数的圈。
[情况假定]
(1)若给定图是一个DAG,就可以按拓扑序给顶点编号,并利用这条递推关系式计算出d。
(2)若给定图含有圈,就无法依赖这样的顺序进行计算。在这种情况下,
记当前到顶点i的最短路长度为d[i],并设定初值d[s] = 0, d[i] = INF,再不断使用
递推关系式更新d的值,就可以算出新的d了。
(注意:只要图中不出现负圈,这样的更新操作一定是有限的。并且更新结束后,d就是所求的最短路径)
[bellman-ford算法代码实现]
1 //定义边结构体:记录边:从顶点from指向顶点to的权值为cost的边 2 struct edge 3 { 4 int from;//顶点from 5 int to;//顶点to 6 int cost;//顶点from到顶点to的边权from-to:cost 7 }; 8 9 edge es[maxn];//声明边集合,其中maxn为已定义的常量 10 11 int d[maxn];//最短距离 12 int v,e;//定点数v,边数e 13 14 //求解从顶点s出发到所有点的最短距离 15 void shortest_path(int s) 16 { 17 //初始化d[] 18 for (int i = 0; i < v; ++i) 19 d[i] = INF;//其中INF为已定义的无穷大常量 20 d[s] = 0; 21 22 //更新d[],如果遍历一遍不再更新,则退出循环,说明所有顶点到s的最短距离均以确定。 23 while (true) 24 { 25 bool update = false; 26 for (int i = 0; i < e; ++i) 27 { 28 edge te = es[i]; 29 if (d[te.from] != INF && d[te.to] > d[te.from] + te.cost) 30 { 31 d[te.to] = d[te.from] + te.cost; 32 update = true; 33 } 34 } 35 if (!update) 36 break; 37 } 38 }
[总结]
(1)Bellman-Ford算法中,如果图不存在负圈,那么最短路计算不会经过同一个顶点两次(也就是说,最多通过|v| - 1条边)
,while (true)的循环最多执行|v| - 1次,因此复杂度为O(|v|*|e|)
(2)反之,如果存在从s可达的负圈,那么再第|v|次循环中也会更新d的值,因此也可以用这个性质来检查负圈。
如果一开始对所有顶点i,都把d[i] 初始化为0,那么可以检查出所有的负圈。
[检测负圈代码实现]
1 //如果返回true则存在负圈 2 bool find_negative_loop() 3 { 4 //初始化 5 memset(d, 0, sizeof d); 6 7 //更新 8 for (int i = 0; i < v; ++i) 9 { 10 for (int j = 0; j < e; ++j) 11 { 12 edge te = es[j]; 13 if (d[te.to] > d[te.from] +te.cost) 14 { 15 d[te.to] = d[te.from] +te.cost; 16 if (i == v - 1) //如果第v次仍然更新了,则存在负圈 17 return true; 18 } 19 } 20 } 21 return false; 22 }
[进阶]
[spfa算法]:bellman-ford算法的队列优化算法的别称。通常用于求含负权边的单源最短路径,以及
判断负权环。
复杂度O(|v|*|e|)
[spfa算法代码实现]
1 typedef pair<int,int> pii; 2 vector<pii> G[u];//邻接表建图 3 bool vis[maxn]; 4 int d[maxn]; 5 //初始化略 6 //更新 7 while (!q.empty()) 8 { 9 int u = q.front(); 10 q.pop(),vis[u] = false; 11 //vector<pii>::iterator it; 12 //相当于for (; it != G[u].end(); ++it),但是前者c++11才可支持 13 for (auto it:G[u]) 14 { 15 if (d[it.fi] > d[u] + it.se)//fi:记录顶点v;se:记录u-vd的cost:w 16 { 17 d[it.fi] = d[u] + it.se; 18 //vis[]表示该点是否已经进入过队列 19 if (!vis[it.fi])//如果没进入过,则 20 { 21 vis[it.fi] = true;//标志进入过 22 ++mark[it.fi];//记录图是否出现负环 23 q.push(it.fi); 24 if (mark[it.fi] > V)//进入超过顶点数V次,说明是个负环 25 return false;//出现负环return false; 26 } 27 } 28 } 29 return true; 30 }
[疑问]
为什么第v次仍然更新,则存在负圈? 为什么不是能多次更新,则存在负圈? 如何证明?
2.Dijkstra算法
[分析bellman-ford算法]
考虑无负边的情况下,在bellman-ford算法中,如果d[i]还不是最短距离的话,
那么即使进行d[j] = d[i] + cost[i][j]的更新,d[j]也不会变成最短距离。而且,
即使d[i]没有变化,每一次循环也要检查一遍从i出发的所有边。这显然是很浪费时间的。
因此可以对bellman-ford算法进行如下修改:
(1)找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离。
(2)此后不需要再关心(1)中“最短距离已经确定的顶点”。
在(1)和(2)中提到的“最短距离已经确定的顶点”要怎么得到是问题的关键。在最开始时,
只有起点的最短距离是确定的。而在尚未使用过的顶点中,距离d[i]最小的顶点就是最短距离
已经确定的顶点。这是由于不存在负边,所以d[i]不会在之后更新中变小。这个算法即Dijkstra算法。
[dijkstra算法代码实现]:邻接矩阵
1 int cost[maxn][maxn];//cost[u][v]表示边e = (u,v)的权值(不存在这条边时,将之设置为INF) 2 int d[maxn];//顶点s出发的最短距离 3 bool used[maxn];//已经使用过的图 4 int V;//顶点数 5 6 //定义填充任何数据函数,memset有限制,只能填写0/-1 7 template <typename T> 8 void fill(T* start, T* end, T fnum) 9 { 10 for (; start != end; ++start) 11 *start = fnum; 12 } 13 14 //求从起点s出发到各个顶点的最短距离 15 void dijkstra(int s) 16 { 17 //初始化 18 fill<int>(d, d + V, INF); 19 fill<bool>(used, used + V, false); 20 d[s] = 0; 21 22 //更新 23 while (true) 24 { 25 int v = -1; 26 //从尚未使用过的顶点中选择一个距离最小的顶点 27 for (int u = 0; u < V; ++u) 28 if (!used[u] && (v == -1 || d[u] < d[v])) 29 v = u; 30 31 if (v == -1)//如果v == -1表示所有顶点均已使用,退出循环 32 break; 33 used[v] = true;//标志该顶点,表示已经使用 34 35 for (int u = 0; u < V; ++u) 36 d[u] = min(d[u], d[v] + cost[v][u]); 37 } 38 }
[总结]
(1)使用邻接矩阵实现的Dijkstra算法的复杂度是O(|v|*|v|)。使用邻接表的话,更新最短距离
只需要访问每条边一次即可,因此这部分的复杂度O(|e|)。但是每次要枚举所有的顶点来查找下一个
使用的顶点,因此最终复杂度还是O(|v|*|v|)。在|e|比较小时,大部分的时间花在了查找下一个使用的顶点上
,因此需要使用合适的数据结构对其进行优化。
(2)需要优化的是数值的插入(更新)和取出最小值两个操作,因此使用堆就可以了。把每个顶点当前的最短距离用堆维护
,在更新最短距离时,把对应元素往根的方向移动以满足堆的性质。而每次从堆中取出最小值就是下一次要使用的顶点。
这样堆中元素共有O(|v|)个,更新和取出数值的操作有O(|e|)次,因此整个算法复杂度为O(log|v|*|e|)。
[进阶]
[优化部分代码实现]:使用STL的priority_queue实现
1 struct edge 2 { 3 int to; 4 int cost; 5 }; 6 7 typedef pair<int,int> pii;//first是最短距离 second是顶点编号 8 9 int V; 10 vector<edge> G[maxn]; 11 int d[maxn]; 12 13 void dijkstra(int s) 14 { 15 //通过指定greater<pii>参数,堆按照first从小到大的顺序取出值 16 priority_queue<pii, vector<pii>, greater<pii> > pq; 17 //初始化 18 fill(d, d + V, INF); 19 d[s] = 0; 20 pq.push(pii(0,s)); 21 22 //更新 23 while (!pq.empty()) 24 { 25 pii p = pq.top(); 26 pq.pop(); 27 int v = p.second; 28 if (d[v] < p.first) 29 continue; 30 for (int i = 0; i < G[v].size(); ++i) 31 { 32 edge e = G[v][i]; 33 if (d[e.to] > d[v] + e.cost) 34 { 35 d[e.to] = d[v] + e.cost; 36 pq.push(pii(d[e.to], e.to)); 37 } 38 } 39 } 40 }
(3)相对于bellman-ford算法O(|v|*|e|)的复杂度,Dijkstra算法的复杂度是O(log|v|*|e|),
可以更加高效地计算最短路的长度。但是,在图中存在负边的情况下,Dijkstra算法就无法正确求解问题,
还是需要使用bellman-ford算法
[疑问]
为什么bellman-ford能处理负环,不是只能判断吗?怎么处理,具体代码实现是什么?
[多源最短路]:任意两点之间的最短路问题
1.Floyd-Warshall算法
[分析]
(1)DP求解任意两点间的最短路问题。只使用顶点0~k和i,j的情况下,记i到j的最短路长度为d[k + 1][i][j]。
k = -1时,认为只使用i,j,所以d[0][i][j] = cost[i][j]。
(2)只使用顶点0~k归约到只使用0~k - 1的问题上。
只使用0~k时,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。
不经过顶点k的情况下,d[k][i][j] = d[k - 1][i][j]。
通过顶点k的情况下,d[k][i][j] = min(d[k - 1][i][j],d[k - 1][i][k] + d[k -1][k][j])
因为只涉及k 与 k - 1可以压掉这一状态
d[i][j] = min(d[i][j],d[i][k] + d[k][j]);
这个算法叫做Floyd-Warshall算法,可以在O(|v|*|v|*|v|)时间内求得所有两电间的最短路长度。
(注意:Floyd-Warshall算法和Bellman-Ford算法一样,可以处理边是负数的情况。而判断图中是否有负圈,只需检查,是否存在d[i][j]是负数的顶点i就可以了)
[Floyd-Warshall代码实现]
1 int d[maxn][maxn];//d[u][v] 表示边e = (u,v)的权值(不存在时设为INF,不过d[i][i] = 0) 2 int V;//顶点数 3 4 void warshall_floyd() 5 { 6 for (int k = 0; k < V; ++k) 7 for (int i = 0; i < V; ++i) 8 for (int j = 0; j < V; ++j) 9 d[i][j] = min(d[i][j],d[i][k] + d[k][j]); 10 }
(注:通过三重循环可以非常简单的求出所有两点间的最短路长度。由于实现起来非常简单,如果复杂度在可以承受的范围内,单源最短路也可以使用Floyd-Warshall算法进行求解)
[路径还原]
[简介]:最短路问题,前面基本都在求最短距离。虽然许多问题只需要输出最短距离就可以了,但是也有的题目需要求解最短路的路径。
以Dijkstra算法为例,求最短路径。求解最短距离时,满足d[j] = d[k] + cost[k][j]的顶点k,就是最短路上顶点j的前趋结点
,因此通过不断寻找前趋结点就可以恢复出最短路。时间复杂度为O(|E|)。
[分析]:如果用prev[j]来记录最短路上顶点j的前趋,那么就可以在O(|v|)的时间内完成最短路的恢复。在d[j]被d[j] = d[k] + cost[k][j]更新时
,修改prev[j] = k;这样就可以求得prev数组。在计算从s出发到j的最短路时,通过prev[j]就可以知道顶点j的前趋,因此不断把j换成prev[j]直到
j = s为止就可以了。Bellman-ford算法和Floyd-Warshall算法都可以用类似的方法进行最短路的还原。
[路径还原代码实现]:dijkstra算法
1 int prev[maxn];//最短路上的前趋顶点 2 3 //求从起点s出发到各个顶点的最短距离 4 void dijkstra(int s) 5 { 6 //初始化 7 fiil(d, d + V, INF); 8 fill(used, used + V, false); 9 fill(prev, prev + V, - 1); 10 d[s] = 0; 11 12 //更新 13 while (true) 14 { 15 int v = -1; 16 for (int u = 0; u < V; ++u) 17 if (!used[i] && (v == -1 || d[u] < d[v])) 18 v = u; 19 20 if (v == -1) 21 break; 22 used[v] = true; 23 24 for (int u = 0; u < V; ++u)//更新,永远更新不到s,所以prev[s] = -1 25 if (d[u] > d[v] + cost[v][u]) 26 { 27 d[u] = d[v] + cost[v][u]; 28 prev[u] = v; 29 } 30 } 31 } 32 33 //到顶点t的最短路 34 vector<int> get_path(int t) 35 { 36 vector<int> path; 37 for (; t != -1; t = prev[t]) 38 path.push_back(t);//不断沿着prev[t]走直到t = s; 39 //按照这样得到的时t到s的顺序,所以翻转path 40 reverse(path.begin(), path.end()); 41 return path; 42 }
[疑问]
Bellman-ford算法如何进行最短路还原?Floyd-Warshall算法如何进行最短路还原?

浙公网安备 33010602011771号