OSDN Git Service

feat: update bfs, ref e-maxx-eng
authorIr1d <sirius.caffrey@gmail.com>
Wed, 22 Aug 2018 16:25:19 +0000 (00:25 +0800)
committerIr1d <sirius.caffrey@gmail.com>
Wed, 22 Aug 2018 16:25:19 +0000 (00:25 +0800)
docs/graph/traverse.md
docs/search/bfs.md

index 3885e2c..48397a5 100644 (file)
@@ -1,18 +1,20 @@
-# dfs
+## dfs
 
-## 树上 dfs
+[DFS 基础](/search/dfs)
+
+### 树上 dfs
 
 在树上 dfs 是这样的一个过程:先访问根节点,然后分别访问根节点每个儿子的子树
 
 可以用来求出每个节点的深度、父亲等信息
 
-## dfs 序列
+### dfs 序列
 
 dfs 序列是指 dfs 调用过程中访问的节点编号的序列。
 
 我们发现,每个子树都对应 dfs 序列中的连续一段(一段区间)。
 
-## 括号序列
+### 括号序列
 
 dfs 进入某个节点的时候记录一个左括号 `(`,退出某个节点的啥时候记录一个右括号 `)`。
 
@@ -20,25 +22,25 @@ dfs 进入某个节点的时候记录一个左括号 `(`,退出某个节点的
 
 相邻两个节点的深度相差 1。
 
-## 二叉树上 dfs
+### 二叉树上 dfs
 
 (图待补)
 
-### 先序遍历
+#### 先序遍历
 
 先访问根,再访问子节点。
 
-### 中序遍历
+#### 中序遍历
 
 先访问左子树,再访问跟,再访问右子树。
 
-### 后序遍历
+#### 后序遍历
 
 先访问子节点,再访问根。
 
-### 已知两个可以求第三个
+#### 已知两个可以求第三个
 
-## 一般图上 dfs
+### 一般图上 dfs
 
 对于非连通图,只能访问到起点所在的连通分量。
 
@@ -50,19 +52,21 @@ dfs 进入某个节点的时候记录一个左括号 `(`,退出某个节点的
 
 dfs 树有很多性质,比如用来求 [强连通分量](/graph/scc)
 
-# bfs
+## bfs
+
+[BFS 基础](/search/bfs)
 
-## 树上 bfs
+### 树上 bfs
 
 从树根开始,严格按照层次来访问节点。
 
 bfs 过程中也可以顺便求出各个节点的深度和父亲节点。
 
-## bfs 序列
+### bfs 序列
 
 类似 dfs 序列,bfs 序列是指在 bfs 过程中访问的节点编号的序列。
 
-## 一般图上 bfs
+### 一般图上 bfs
 
 同样,如果原图不连通,只能访问到起点所在的连通分量。
 
index 29f533e..0aef1c7 100644 (file)
@@ -1,10 +1,16 @@
-bfs 全称是 Breadth First Search
+BFS 全称是 [Breadth First Search](https://en.wikipedia.org/wiki/Breadth-first_search)
 
-是一种图的遍历算法
+是图上最基础、最重要的搜索算法之一
 
 所谓宽度优先。就是每次都尝试访问同一层的节点。
 如果同一层都访问完了,再访问下一层。
 
+这样做的结果是,BFS 算法找到的路径是从起点开始的 ** 最短 ** 合法路径。换言之,这条路所包含的边数最小。
+
+在 BFS 结束时,每个节点都是通过从起点到该点的最短路径访问的。
+
+算法过程可以看做是图上火苗传播的过程:最开始只有起点着火了,在每一时刻,有火的节点都向它相邻的所有节点传播火苗。
+
 ## 实现
 
 伪代码:
@@ -30,20 +36,43 @@ C++:
 ```c++
 void bfs(int u) {
   while (!Q.empty()) Q.pop();
-  Q.push(u); vis[u] = 1;
+  Q.push(u); vis[u] = 1; d[u] = 0; p[u] = -1;
   while (!Q.empty()) {
     u = Q.pop() {
       for (int i = head[u]; i; i = e[i].x) {
         if (!vis[ e[i].t ]) {
           Q.push(e[i].t);
           vis[ e[i].t ] = 1;
+          d[ e[i].t ] = d[u] + 1;
+          p[ e[i].t ] = 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("");
+}
 ```
 
+具体来说,我们用一个队列 Q 来记录要处理的节点,然后开一个 $vis[]$ 布尔数组来标记某个节点是否已经访问过了。
+
+开始的时候,我们把起点 s 以外的节点的 vis 值设为 0,意思是没有访问过。然后把起点 s 放入队列 Q 中。
+
+之后,我们每次从队列 Q 中取出队首的点 u,把 u 相邻的所有点 v 标记为已经访问过了并放入队列 Q。
+
+直到某一时刻,队列 Q 为空,这时 BFS 结束。
+
+在 BFS 的过程中,也可以记录一些额外的信息。比如上面的代码中,d 数组是用来记录某个点到起点的距离(要经过的最少边数),p 数组是记录从起点到这个点的最短路上的上一个点。
+
+有了 d 数组,可以方便地得到起点到一个点的距离。
+
+有了 p 数组,可以方便地还原出起点到一个点的最短路径。上面的 restore 函数就是在做这件事:restore(x) 输出的是从起点到 x 这个点所经过的点。
 
 时间复杂度 $O(n + m)$
 
@@ -51,6 +80,26 @@ void bfs(int u) {
 
 ## open-closed 表
 
-在实现 bfs 的时候,我们把未被访问过的节点放在一个称为 open 的容器中,而把已经访问过了的节点放在 closed 容器中。
+在实现 BFS 的时候,我们把未被访问过的节点放在一个称为 open 的容器中,而把已经访问过了的节点放在 closed 容器中。
+
+## [在树 / 图上 BFS](/graph/traverse)
+
+## 应用
+
+- 在一个无权图上求从起点到其他所有点的最短路径。
+- 在 $O(n+m)$ 时间内求出所有连通块。(我们只需要从每个没有被访问过的节点开始做 BFS,显然每次 BFS 会走完一个连通块)
+- 如果把一个游戏的动作看做是状态图上的一条边(一个转移),那么 BFS 可以用来找到在游戏中从一个状态到达另一个状态所需要的最小步骤。
+- 在一个边权为 0 / 1 的图上求最短路。(需要修改入队的过程,如果某条边权值为 0,且可以减小边的终点到图的起点的距离,那么把边的起点加到队列首而不是队列尾)
+- 在一个有向无权图中找最小环。(从每个点开始 BFS,在我们即将抵达一个之前访问过的点开始的时候,就知道遇到了一个环。图的最小环是每次 BFS 得到的最小环的平均值。)
+- 找到一定在 $(a, b)$ 最短路上的边。(分别从 a 和 b 进行 BFS,得到两个 d 数组。之后对每一条边 $(u, v)$,如果 $d_a[u]+1+d_b[v]=d_a[b]$,则说明该边在最短路上)
+- 找到一定在 $(a, b)$ 最短路上的点。(分别从 a 和 b 进行 BFS,得到两个 d 数组。之后对每一个点 v,如果 $d_a[u]+d_b[v]=d_a[b]$,则说明该点在最短路上)
+- 找到一条长度为偶数的最短路。(我们需要一个构造一个新图,把每个点拆成两个新点,原图的边 $(u, v)$ 变成 $((u, 0), (v, 1))$ 和 $((u, 1), (v, 0))$。对新图做 BFS,$(s, 0)$ 和 $(t, 0)$ 之间的最短路即为所求)
+
+
+## 例题
+
+- [LOJ#2317. 「NOIP2017」奶酪](https://loj.ac/problem/2317)
+
+## 参考
 
-## [在树 / 图上 bfs](/graph/traverse)
\ No newline at end of file
+https://cp-algorithms.com/graph/breadth-first-search.html