图的搜索和遍历算法,即 BFS 和 DFS.
# 图的遍历
图的遍历是图的基本问题。其指的是如何通过某个特定的顺序或规则,完成对图中所有点的访问。更具体些,对一个图, 我们通常给出图上的一个源点 (source), 如何从 出发到达每一个点是我们关注的问题。我们希望得到一种通用的遍历规则。
关于此,我们常有两种方法,分别称为广度优先搜索 (breadth-first search, BFS) 和深度优先搜索 (depth-firth search, DFS)。图算法中的很多想法都源自这两种基本的图遍历方法。
# 广度优先搜索 (BFS)
# 算法代码和思路
我们用一个队列 Q 来记录要处理的节点,然后开一个布尔数组 vis[] 来标记是否已经访问过某个节点。
开始的时候,我们将所有节点的 vis 值设为 0 , 表示没有访问过;然后把起点 s 放入队列 Q 中并将 vis[s] 设为 1 .
之后,我们每次从队列 Q 中取出队首的节点 u , 然后把与 u 相邻的所有节点 v 标记为已访问过并放入队列 Q .
循环直至当队列 Q 为空,表示 BFS 结束。
下面给出广度优先搜索的代码:
void bfs(int u) {  | |
while (!Q.empty()) Q.pop();  | |
Q.push(u);  | |
vis[u] = 1;  | |
d[u] = 0;  | |
p[u] = -1;  | |
while (!Q.empty()) {  | |
u = Q.front();  | |
Q.pop();  | |
for (int i = head[u]; i; i = e[i].nxt) {  | |
if (!vis[e[i].to]) {  | |
Q.push(e[i].to);  | |
vis[e[i].to] = 1;  | |
d[e[i].to] = d[u] + 1;  | |
p[e[i].to] = u;  | |
      } | |
    } | |
  } | |
} | |
void restore(int x) {  | |
vector<int> res;  | |
for (int v = x; v != -1; v = p[v]) {  | |
res.push_back(v);  | |
  } | |
std::reverse(res.begin(), res.end());  | |
for (int i = 0; i < res.size(); ++i) printf("%d", res[i]);  | |
puts("");  | |
} | 
# 性质
# 复杂度
- 时间复杂度:.
 - 空间复杂度:.
 
# 深度优先搜索
# 算法描述
对最新发现的顶点,如果存在以其为起点但未探测到的边,就继续探测下去。当所有顶点都被探索后,就按照发现该顶点的顺序进行回溯,直到某个顶点存在新的为探测到的边。直到所有的顶点都被探测之后,算法停止。这样的算法称为深度优先搜索 (depth-first search, DFS)。
在深度优先搜索中,每次扫描顶点 的邻接表发现新顶点 时,将 的前辈 取 . 这样,所有顶点的先辈子图形成一个森林,称为深度优先林 (depth-first forest). 深度优先林中的每一棵树称为深度优先树 (depth-first tree).
此外,在深度优先搜索中,我们在搜索过程中为每个顶点加两个时间戳。当发现一个顶点 (将顶点 置灰色) 时,为其添加时间戳 . 当结束检查 的邻接表 (将顶点 置黑色) 时,为其添加时间戳 . 这样的时间戳为深度优先搜索的拓展起了很大的作用。
下面,我们采用代码表示深度优先搜索的过程:
// initialize | |
for (Vertex u : G) {  | |
u.color = WHITE;  | |
u.pi = null;  | |
} | |
int time = 0;  | |
for (Vertex u : G) {  | |
if (u.color == WHITE) {  | |
dfs_visit(u);  | |
  } | |
} | |
void dfs_visit(Vertex u) {  | |
u.color = GRAY;  | |
u.d = ++time; // discovered time  | |
for (Vertex v : u.adj) { // vertex in u's adjacent nodes  | |
if (v.color == WHITE) {  | |
v.pi = u;  | |
dfs_visit(v);  | |
    } | |
  } | |
u.color = black;  | |
u.f = ++time; // finished time  | |
} | 
注意到,我们维护的域越多,可以得到的(冗余)信息也就越多。如果只是单纯进行图遍历,那么发现和完成时刻是不需要维护的。由此,我们可以写一个简单的 DFS. (就是一个迭代)
struct vertex {  | |
int index;  | |
int label; // if visited  | |
vector<vertex*> adj;  | |
};  | |
void dfs(vertex* root) {  | |
  // do something | |
for (auto v : root) {  | |
if (v.label == 0) dfs(v);  | |
  } | |
} | 
# 性质
# 复杂度
- 时间复杂度:.
 - 空间复杂度:.
 
# 括号定理
# 白色路径定理
在一个图 的深度优先森林中,顶点 是顶点 的后裔,当且仅当搜索过程中于时刻 发现 时,可以从顶点 出发,经过一条完全由白色顶点组成的路径到达.
# 深度优先林中边的分类
在深度优先林中,我们对反向边进行分类。可以分为以下四种:
- 树边 (tree edge):
 - 后向边 (back edge)
 - 前向边
 - 交叉边
 
# 应用
- 连通性判断
 - 判环
 - 拓扑排序
 
# DAG & 拓扑排序
# DAG
有向无环图 (directed acyclic graph, DAG).
# 拓扑排序
采用 DFS 实现。返回时间的倒序就是一个可行的拓扑序。
# 强连通性
# 强连通图
# Kosaraju 算法
是一个在线性时间 内寻找一个有向图中的强连通分量的算法。
算法如下:
- 对图中的每个顶点 , 标记 
vis[u] = 0(unvisited). 同时创建一个存放顶点的空栈L. - 对图中的每个顶点 , 执行 
visit(u), 其中visit(u)定义如下:void visit(Vertex u) {
if (vis[u] == 0) {
vis[u] = 1; // visited
for (Vertex v : u.outNeighbor) visit(v);
L.push(u);
}} - 对 
L中的每个顶点 , 执行assign(u, u), 
For each vertex u of the graph, mark u as unvisited. Let L be empty.
For each vertex u of the graph do Visit(u), where Visit(u) is the recursive subroutine:
If u is unvisited then:
Mark u as visited.
For each out-neighbour v of u, do Visit(v).
Prepend u to L.
Otherwise do nothing.
For each element u of L in order, do Assign(u,u) where Assign(u,root) is the recursive subroutine:
If u has not been assigned to a component then:
Assign u as belonging to the component whose root is root.
For each in-neighbour v of u, do Assign(v,root).
Otherwise do nothing.
# Tarjan 算法
此处的 Tarjan 算法是求强连通分量的 Tarjan 算法。
Tarjan 算法的核心是维护两个标志量:
dfn[u]: 表示 DFS 中进入节点u的时刻。low[u]: 表示 DFS 中,u的子树中能够回溯到的最早的已经在栈中的结点,即dfn值最小的返祖边指向的点。
于是,从根开始的一条路径上的 dfn 严格递增, low 严格非降。
void TarjanSearch(int u) {  | |
vis[u] = true;  | |
low[u] = dfn[u] = ++dfncnt;  | |
visStack.push(u);  | |
for(int v=0;v<n;v++) {  | |
if (edge[u][v] == 1) {  | |
if (!vis[v]) {  | |
TarjanSearch(v); // 搜索  | |
low[u]=min(low[u],low[v]); // 回溯  | |
      } | |
else if (visStack.has(v))  | |
low[u]=min(low[u],dfn[v]);  | |
    } | |
  } | |
} | 
若存在 dfn[u] == low[u] , 那么 u 及其上方的节点 (直到 u 本身) 构成一个强连通分支。