%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# DFS 与 BFS
## DFS 深度优先遍历
略。
## BFS 广度优先遍历
![[_attachment/00-数据结构与算法笔记/专题总结/DFS & BFS 搜索.assets/IMG-DFS & BFS 搜索-623BA5F53E79BA14E625A25070945B1C.png|470]]
在无权图中,BFS 以层序遍历,**每一层代表 "一步"**,因此 **BFS 首次访问某一位置时,即可得到从源点到该位置的最少步数**。
<br><br>
# DFS 与 BFS 复杂度分析
给定图 $G=(V,E)$ :
- 采用 **==邻接表==建图** 时:
- 时间复杂度:$O(|V|+|E|)$,图中**每个顶点会访问一次**,然后 **==枚举各顶点的每条边==**,故**每条边也最多被访问一次**。
- 空间复杂度:$O(|V|+|E|)$ ,存储 V 个顶点以及所有边。
- 采用 **==邻接矩阵==建图** 时:
- 时间复杂度:$O(|V|^2)$,图中**每个顶点会访问一次**,然后**枚举图中所有顶点**,检查是否存在边。
- 空间复杂度:$O(|V|^2)$
图可以是 **无向图** 或 **有向图**。
> [!NOTE] 从一个顶点出发进行 DFS/BFS,搜索整个连通分量
>
> 在 "**不访问重复顶点**" 的情况下地**走一遍**可得到:
>
> - **图的生成树/支撑树**(一个 **==极大连通分量==**)
> - 会 **==访问到所有顶点==而得到生成树**;
> - **不一定会 "==走过==" 所有边,但一定会 "==见过==" 所有边**!=> 每一步都会**为每个顶点遍历其邻接边**
>
> - 若能够遇见 "**已访问过的顶点**" ,意味着**图中存在环**(从一个顶点出发,到另一个顶点存在两条路径)
<br><br>
# DFS 实现模版
DFS 有两种写法,差别在于 "**对将访问位置的合法性检查的时机**" :
1. 写法一:在 "**==DFS 函数体内==**" 首先进行位置合法性检查;
2. 写法二:在 "**==调用 DFS 之前==**" 先进行位置合法性检查,确保传递给 `dfs()` 的是有效位置。
> [!NOTE] 建议:`DFS` 函数体内始终先进行 "可行性" 检查,独立于外部判断。
![[_attachment/00-数据结构与算法笔记/专题总结/DFS & BFS 搜索.assets/IMG-DFS & BFS 搜索-85EAD0388F614795BE1FF7D90D6E2C86.png|347]]
#### 一般图 DFS
```cpp
// 一般图上DFS
void dfs(vector<vector<int>>& graph, int start, int dest) { // graph是邻接表.
int n = graph.size();
vector<bool> visited(n, false);
auto dfs = [&](auto& self, int u) {
if (visited[u]) return; // 若保证传入DFS的都是未访问的有效位置, 则可省略.
visited[u] = true;
// process(u);
if (u == dest) return; // 抵达终点
for (const int v : graph[u]) {
if (visited[v]) continue;
self(self, v);
}
// visited[u] = false; // opt.可选, 撤销对已访问位置的标记, 适用于回溯的情况
};
dfs(dfs, start);
}
```
#### 网格图 DFS
```cpp
// 网格图DFS
class DFS_Grid {
static constexpr int dirs[4][2]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// 递归实现
static void DFS(vector<vector<int>>& grid, int sx, int sy) {
int m = grid.size(), n = grid[0].size();
vector<vector<bool>> visited(m, vector<bool>(n, false));
auto dfs = [&](auto& self, int x, int y) {
if (x < 0 || x >= m || y < 0 || y >= n) return;
if (visited[x][y]) return;
visited[x][y] = true;
// process(x, y);
for (const auto [dx, dy] : dirs) {
self(self, x + dx, y + dy);
}
// visited[x][y] = false; // opt.可选, 撤销对已访问位置的标记, 适用于回溯的情况
};
dfs(dfs, sx, sy);
}
// 迭代+栈实现.
static void DFS_by_iter(vector<vector<int>> &graph, int sx, int sy) {
if (graph.empty()) return;
int m = graph.size(), n = graph[0].size();
if (sx < 0 || sx >= m || sy < 0 || sy >= n) return;
stack<pair<int, int>> s({{sx, sy}});
vector<vector<bool>> visited(m, vector<bool>(n, false));
visited[sx][sy] = true; // 入栈时就必须标记已访问.
while (!s.empty()) {
auto [x, y] = s.top(); s.pop();
// process(graph, x, y);
for (const auto& [dx, dy]: dirs) { // 遍历各个方向
int nx = x + dx, ny = y + dy; // 邻居节点坐标
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
if (visited[nx][ny]) continue;
visited[x][y] = true; // 入栈时就必须标记已访问, 避免重复入栈.
s.emplace(nx, ny); // 邻居节点入栈
}
}
}
};
```
<br><br>
# BFS 实现模版
> [!caution] BFS 必须在 "**==顶点入队时==**" 就立即标记 `visited` 已访问,否则会**导致同一顶点==多次重复地入队==**!(力扣上会直接超时)
>
> 即 `visited[v] = true` 必须紧邻 "邻居节点入队" 的位置!
>
> ![[_attachment/00-数据结构与算法笔记/专题总结/DFS & BFS 搜索.assets/IMG-DFS & BFS 搜索-4982087C1B13B3815864992DC6105F8F.png|627]]
### 一般图中 BFS
```cpp
// 基础BFS.
void bfs(vector<vector<int>>& graph, int start) { // graph是邻接表.
int n = graph.size();
vector<bool> visited(n, false);
queue<int> que{{start}}; // que记录已标记访问的节点, 这些节点的邻居待访问
visited[start] = true;
while (!que.empty()) {
int u = que.front(); que.pop();
for (const int v : graph[u]) {
if (visited[v]) continue;
visited[v] = true;
que.emplace(v);
// process(graph, v); // 处理顶点.
}
}
}
```
### 网格图上 BFS
```cpp
// 基础BFS.
void bfs(vector<vector<int>>& grid, int sx, int sy) { // `起点(sx, sy)`
static constexpr int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int m = grid.size(), n = grid[0].size();
if (m == 0 || n == 0) return;
if (sx < 0 || sx >= m || sy < 0 || sy >= n) return;
// 初始时, 将起点加入栈, 并标记起点为"已访问"
// queue中存储的必须是合法的、下一步待访问的位置. 因此初始时, 需要对(sx,sy)非法的情况特判.
vector<vector<bool>> visited(m, vector<bool>(n, false));
queue<pair<int, int>> que({{sx ,sy}}); // que记录已标记访问的节点, 这些节点的邻居待访问
visited[sx][sy] = true;
while (!que.empty()) {
auto [x, y] = que.front(); que.pop();
for (const auto& [dx ,dy] : dirs) {
int nx = x + dx, ny = y + dy;
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
if (visited[nx][ny]) continue;
if (grid[nx][ny] == 1) continue; // 障碍物, 不可访问.
visited[nx][ny] = true; // 一旦入队列, 需立即标记"已访问". 防止同一位置被多次重复入队.
que.emplace(nx, ny);
// process(graph, nx, ny); // 处理该位置
}
}
}
```
#### 🚨 非常低效的实现
```cpp
// 错误实现: 差评, 非常低效, 会导致大量节点重复加入队列,
void bfs(vector<vector<int>> grid, int start_x, int start_y) {
int m = grid.size(), n = grid[0].size();
const int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
queue<pair<int, int>> q;
vector<vector<bool>> visited(m, vector<bool>(n, false));
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
if (visited[nx][ny]) continue;
if (grid[nx][ny] == 1) continue; // 障碍物, 不可访问
// 标记访问(nx, ny), 将该位置加入搜索.
visited[nx][ny] = true;
for (auto [dx, dy] : dirs) {
int nx = x + dx, ny = y + dy;
q.emplace(nx, ny);
}
}
}
```
<br><br>
# 回溯法
> 回溯(backtracking):**穷举式的搜索算法**。
回溯算法本质上就是 "**==DFS==**":**采用试错的思想**,在搜索尝试过程中寻找问题的解,**无法进一步搜索时**则 "**回退**",转而搜索其他路径。
> [!NOTE] 回溯法适用的问题:
>
> - **组合问题**:n 个数中找出满足条件的k 个数的集合
> - **子集问题**:n 个数中有多少满足条件的子集
> - **排列问题**:n 个数按一定规则排列,所有可能的排列方式
> - **切割问题**:一个字符串按指定规则切割,有多少种切割方式
> - **棋盘问题**:解数独、N 皇后
> - **路径问题**:找出**所有满足条件的路径**
> [!example] 全排列问题的搜索树
> 参见 [[^1]]
> ![[_attachment/00-数据结构与算法笔记/专题总结/DFS & BFS 搜索.assets/IMG-DFS & BFS 搜索-915B2A1775E3586DD2816B3D2501CD9D.png|524]]
## 回溯法模版
参见 [^1]
```cpp
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (枚举当前可选的元素列表) {
处理节点, 合法性检查;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果;
}
}
```
> [!NOTE] 注意 DFS 中对 "位置合法性检查" 的两种写法风格,体现在 "**回溯**" 如下。上述的回溯模版是**风格 2**。
```cpp
// 风格一:
void dfs(int u) {
if (节点u不可达) return;
// 记录节点u在当前路径上
path.push_back(u);
if (u为目标终点) {
ans.push_back(path);
path.pop_back(); // 回撤
}
// 2.找邻居
for (节点u的各个邻居v) {
dfs(v);
}
// 撤回对节点u的记录
path.pop_back();
}
// 风格二: 要求外部调用dfs前先将起点s加入path.
void dfs(int u) {
if (u为目标终点) ans.push_back(path);
for (节点u的各个邻居v) {
if (节点v可达) {
// 记录节点v在当前路径上
path.push(v);
dfs(v);
// 撤回对节点v的记录
path.pop();
}
}
}
```
<br><br>
# 参考资料
# Footnotes
[^1]: [01. 回溯算法知识 \| 算法通关手册(LeetCode)](https://algo.itcharge.cn/09.Algorithm-Base/04.Backtracking-Algorithm/01.Backtracking-Algorithm/#_2-%E4%BB%8E%E5%85%A8%E6%8E%92%E5%88%97%E9%97%AE%E9%A2%98%E5%BC%80%E5%A7%8B%E7%90%86%E8%A7%A3%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95)