Add tSort, prim, dijkstra to Graph conclusion.
This commit is contained in:
@@ -172,3 +172,146 @@ DFS的时间复杂度也与BFS一致。
|
||||
> dfs的应用。
|
||||
|
||||
dfs是图遍历算法中最重要的一个。大量与图相关的算法都是由dfs导出的,比如连通分量分解,拓扑排序等。此外,dfs还可以用来做带权图的最短路径算法框架。
|
||||
|
||||
## 拓扑排序
|
||||
|
||||
> 什么是拓扑排序?
|
||||
|
||||
事物之间往往会有一个依赖关系,因此形成一个了先后次序关系。比如我得先学好`Vector`和`List`,才能来学习依赖于两者的树,从而才能学习依赖于树的图结构。它们之间的这种次序就构成了一个拓扑顺序。
|
||||
|
||||
对于一般的有向图而言,就是要找到一个线性序列,使得对于任意个顶点x,排在它后面的顶点一定不会是当前顶点的前驱顶点。这个序列表示在访问x之前,x的所有前驱顶点一定要首先被访问。这个序列就是原图的一个拓扑排序(Topological Sort)。
|
||||
|
||||
要注意拓扑排序通常都是研究有向图,因为它反映了事物的一个先后次序。无向图不具有这样一个先后的次序,研究它的拓扑排序是没有意义的。
|
||||
|
||||
> 有向无环图与拓扑排序。
|
||||
|
||||
接下来要解决的一个问题就是,什么样的图具有拓扑排序?因为直观上来看,有环图必然是没有拓扑排序的。因此,接下来我们重要要研究的是就是有向无环图(DAG, Directed Acyclic Graph)。
|
||||
|
||||
那么,所有的DAG都具有拓扑排序吗?答案是是的,为了证明这个答案,我劝你还是学完离散数学再来吧......
|
||||
|
||||
> 拓扑排序的构造方法。
|
||||
|
||||
根据拓扑排序的定义进行构造。拓扑排序的每一步,都是寻找依赖项已经完成访问的顶点。因此,作为拓扑排序的起点,必然是一个入度为零的顶点,表示该顶点不依赖于任何其他顶点。从这里我们也可以看出,拓扑排序并不是唯一的,因为同时没有依赖的顶点可能有多个,此时它们都可以等效地加入拓扑排序的序列中。
|
||||
|
||||
在将任意一个准备被访问的顶点加入拓扑排序序列中后,相当于图中依赖于该顶点的其他顶点已经满足了对其的依赖关系,因此可以将该顶点从图中删除,以及删除与该顶点有联系的所有边。接下来的图仍然是一个DAG,从而可以将算法递归地进行下去,直到图中不再有任何顶点为止。
|
||||
|
||||
> 基于dfs的拓扑排序算法
|
||||
|
||||
其实,我们对比拓扑排序的定义以及dfs遍历的次序,可以发现它们之间具有某种相似性。对于拓扑排序而言,每次是选择入度为零的顶点加入拓扑排序序列;而对于dfs而言,只有当一个顶点的所有邻居都访问完毕后,这个顶点才会被标记`VISITED`,这样dfs中第一个被访问的顶点,必然是出度为零的顶点,而倘若没访问结束一个顶点,就将该顶点删除,那么dfs每一步都是访问出度为零的顶点。可以看出,dfs的访问次序,恰好是拓扑排序的逆序。
|
||||
|
||||
其实,上面的结论并不是偶然,而是由dfs的特性决定的--在dfs中,对于当前顶点x,总是需要优先访问完所有依赖于它的顶点,才能进行对x的访问。这恰好是拓扑排序的对称情况。
|
||||
|
||||
为了完成对图的拓扑排序,只需要逆序输出它的dfs访问序列就可以了。为此,我们需要引入一个栈用以延迟缓冲。
|
||||
|
||||
```cpp
|
||||
template <typename Tv, typename Te>
|
||||
bool Graph<Tv, Te>::tSort(int x, Stack<int> &S){
|
||||
status(x) = DISCOVERED;
|
||||
for(int w = firstNeighbor(x); w != -1; w = nextNeighbor(x, w)){
|
||||
switch(status(w)){
|
||||
case UNDISCOVERED:
|
||||
if(!tSort(w, S)) return false;
|
||||
break;
|
||||
|
||||
case DISCOVERED:
|
||||
return false;//cyclic graph
|
||||
|
||||
case VISITED:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
status(x) = VISITED;
|
||||
S.push(x);
|
||||
}
|
||||
```
|
||||
|
||||
代码执行结束后,拓扑排序序列就存储在栈`S`中。需要注意的是,利用dfs的环路检测的性质,可以轻易地判断当前图是否是一个DAG,一旦发现一个环路,就可以立即终止循环,并且报告不可拓扑排序。
|
||||
|
||||
由于该算法是采用dfs的框架,其时间复杂度也和dfs一样,为$O(n + e)$。
|
||||
|
||||
## 双连通域的分解
|
||||
|
||||
## 优先级搜索
|
||||
|
||||
## 最小支撑树
|
||||
|
||||
> 什么是最小支撑树(MST, Minimal Spanning Tree)
|
||||
|
||||
沿用之前对树的定义,树是一个极大无环图和极小连通图。最小支撑树也满足这样的定义。
|
||||
|
||||
对于任意一个无向连通图而言,例如若干城市和它们之间的道路组成的网络,从某一个城市出发到达另一个城市往往具有若干条不同的道路,所谓条条大路通罗马。这个网络的一棵支撑树,就是只通过最少的路径数,就能将该网络的所有城市都连接起来的若干通路。如果再考虑这些不同的通路之间具有不同的权重,例如时间成本不同,具有最小权重和的一棵支撑树,就是最小支撑树(MST)。
|
||||
|
||||
> 如何建立连通图的最小支撑树?
|
||||
|
||||
据邓公所说,蛮力算法需要$O(n^{n-2})$的时间复杂度,是根据Cayley公式,我还比较懵......
|
||||
|
||||
首先考虑一种较为一般的情况。考虑图G顶点集V,V的一个非平凡子集U以及U的补集V\U构成了G的一个割。最小支撑树总是会采用每一割的最短跨越边。
|
||||
|
||||
根据上面的性质,就可以得到构造最小支撑树的算法:总是将原图视作一个割,两个顶点集分别是已经加入到最小支撑树中的顶点和未加入的顶点。通过找到这个割的最短跨越边,从而将一个新的顶点加入到最小支撑树中。这样不断地迭代,知道最小支撑树覆盖了全图的所有顶点。
|
||||
|
||||
这里比较复杂的问题是,如何快速地找到当前割的最短跨越边,为此,可以模仿dijkstra算法,维护一个数组,来保存所有未加入最小支撑树的顶点到最小支撑树的路径,每加入一个新的顶点,将这个数组进行更新。这里并没有真正地用一个数组,而是用每个顶点的`priority`字段来保存这个信息。
|
||||
|
||||
```cpp
|
||||
template <typename Tv, typename Te>
|
||||
void Graph<Tv, Te>::primPU(int x){
|
||||
for(int w = firstNeighbor(x); w != -1; w = nextNeighbor(x, w))
|
||||
if(status(w) == UNDISCOVERED){
|
||||
if(weight(x, w) < priority(w)) priority(w) = weight(x, w);
|
||||
parent(w) = x;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过优先级搜索的框架,即可以完成最小支撑树的生成。
|
||||
|
||||
## 最短路径
|
||||
|
||||
最短路径的定义与应用意义不言而喻,我们直接讨论如何求得最短路径的算法。
|
||||
|
||||
> 最短路径树
|
||||
|
||||
在任意一个带权网络中,考察从源点到其余各个顶点的最短路径,它们之间并不组成任何回路。因此将它们组合在一起,可以构成最短路径树(SPT, Shortest Path Tree)。
|
||||
|
||||
需要注意的是,SPT和MST不一样。
|
||||
|
||||
> Dijkstra算法
|
||||
|
||||
首先来考虑最短路径具有的若干性质。
|
||||
|
||||
考虑从源点s到任意顶点v的最短路径,其路径上还有若干其他顶点。那么该路径上从s到这些顶点的一段,也是从s到这些顶点的最短路径。这个性质是容易证明的,因为否则的话,从s到v还有一条更短的路径,这与一开始的假设矛盾。
|
||||
|
||||
从上面的性质可以看出,为了构造从s到某一顶点的最短路径,首先需要构造从s到该顶点路径上更前面顶点的最短路径。将图中各点按距离s的远近次序由近到远排个序,就构成了最短路径子树序列。
|
||||
因此为了构造从源点s到所有其他顶点的最短路径,需要依次找到距离s最近的$u_1, $u_2, ..., u_k$,从而完成最短路径树的构造。
|
||||
|
||||
+ 首先需要找到距离s最近的顶点$u_1$。实际上,$u_1$是s的邻居中距离s最近的顶点。这是因为,倘若存在s的非邻居顶点$x$,距离s的距离比$u_1$更近,那么它必然通过某个顶点$y$与s连接,$y$是s的邻居顶点。所以,$x$到s的距离为
|
||||
$$
|
||||
dist(x, s) = dist(x, y) + dist(y, s)
|
||||
$$
|
||||
。而
|
||||
$$
|
||||
dist(y, s) > dist(u_1, s)
|
||||
$$
|
||||
,所以$x$到s的距离实际上比$u_1$远,故$u_1$才是距离s最近的顶点。
|
||||
|
||||
+ 已知$u_k$,找到接下来距离s最近的顶点$u_{k + 1}$。这个顶点就是所有的与
|
||||
s以及$u_1$到$u_k$连通的顶点中,距离s最近的一个。这个的证明和前面的原理一致,可以自己试试。
|
||||
|
||||
至此,我们已经归纳地证明了最短路径树序列的构造方法,而这个算法,就是Dijkstra算法。
|
||||
|
||||
同样地,找到当前的具有最短距离的顶点,具有一定的困难。为此,同样引入一个数组来保存前面已经保存了的距离信息,一旦有新的顶点加入最短路径序列中,就将这个数组进行更新。
|
||||
|
||||
```cpp
|
||||
template <typename Tv, typename Te>
|
||||
void Graph<Tv, Te>::dijkstraPU(int x){
|
||||
for(int w = firstNeighbor(x); w != -1; w = nextNeighbor(x, w)){
|
||||
if(status(w) == UNDISCOVERED){
|
||||
if(weight(x, w) + priority(x) < priority(w))
|
||||
priority(w) = weight(x, w) + priority(x);
|
||||
parent(w) = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user