图论_最短路(更新中)

[最短路问题]
定义:起点固定,终点固定,求解两点间最短路径的问题,称为最短路问题。
分类:最短路问题分为单源最短路与多源最短路

[单源最短路]
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算法如何进行最短路还原?
posted @ 2021-07-20 19:25  QingHUIWuHan  阅读(128)  评论(0)    收藏  举报