DFS
本质:一种用于遍历或搜索树或图的算法。所谓深度优先,就是说每次都尝试向更深的节点走。
该算法讲解时常常与 BFS 并列,但两者除了都能遍历图的连通块以外,用途完全不同,很少有能混用两种算法的情况。
关键:
递归调用自身
对其访问过的点打上访问标记,在遍历图时跳过已打过标记的点,以确保 每个点仅访问一次。
伪代码:
DFS(v) // v 可以是图中的一个顶点,也可以是抽象的概念,如 dp 状态等。
在 v 上打访问标记
for u in v 的相邻节点
if u 没有打过访问标记 then
DFS(u)
end
end
end
性质
该算法通常的时间复杂度为 \(O(n+m)\),空间复杂度为 \(O(n)\),其中 \(n\) 表示点数,\(m\) 表示边数。注意空间复杂度包含了栈空间,栈空间的空间复杂度是 \(O(n)\) 的。在平均 \(O(1)\) 遍历一条边的条件下才能达到此时间复杂度,例如用前向星或邻接表存储图;如果用邻接矩阵则不一定能达到此复杂度。
实现
用栈(Stack)为遍历中节点
vector<vector<int>> adj; // 邻接表
vector<bool> vis; // 记录节点是否已经遍历
void dfs(int s) {
stack<int> st;
st.push(s);
vis[s] = true;
while (!st.empty()) {
int u = st.top();
st.pop();
for (int v : adj[u]) {
if (!vis[v]) {
vis[v] = true; // 确保栈里没有重复元素
st.push(v);
}
}
}
}
递归实现
函数在递归调用时的求值如同对栈的添加和删除元素的顺序,故函数调用所占据的虚拟地址被称为函数调用栈(Call Stack),DFS 可用递归的方式实现。
以 邻接表(Adjacency List) 作为图的存储方式:
vector<vector<int>> adj; // 邻接表
vector<bool> vis; // 记录节点是否已经遍历
void dfs(const int u) {
vis[u] = true;
for (int v : adj[u])
if (!vis[v]) dfs(v)
}
以 链式前向星 为例:
void dfs(int u) {
vis[u] = 1;
for (int i = head[u]; i; i = e[i].x) {
if (!vis[e[i].t]) {
dfs(v);
}
}
}
DFS 序列
DFS 序列是指 DFS 调用过程中访问的节点编号的序列。
我们发现,每个子树都对应 DFS 序列中的连续一段(一段区间)。
括号序列
DFS 进入某个节点的时候记录一个左括号 (
,退出某个节点的时候记录一个右括号 )。
每个节点会出现两次。相邻两个节点的深度相差 \(1\)。
一般图上 DFS
对于非连通图,只能访问到起点所在的连通分量。
对于连通图,DFS 序列通常不唯一。
注:树的 DFS 序列也是不唯一的。
在 DFS 过程中,通过记录每个节点从哪个点访问而来,可以建立一个树结构,称为 DFS 树。DFS 树是原图的一个生成树。
BFS
是图上最基础、最重要的搜索算法之一。
所谓宽度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。
这样做的结果是,BFS 算法找到的路径是从起点开始的 最短 合法路径。换言之,这条路径所包含的边数最小。
在 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("");
}
具体来说,我们用一个队列 \(Q\) 来记录要处理的节点,然后开一个布尔数组 vis[]
来标记是否已经访问过某个节点。
开始的时候,我们将所有节点的 \(vis\) 值设为 \(0\),表示没有访问过;然后把起点 \(s\) 放入队列 \(Q\) 中并将 vis[s]
设为 \(1\)。
之后,我们每次从队列 \(Q\) 中取出队首的节点 \(u\),然后把与 \(u\) 相邻的所有节点 \(v\) 标记为已访问过并放入队列 \(Q\)。
循环直至当队列 \(Q\) 为空,表示 BFS 结束。
在 BFS 的过程中,也可以记录一些额外的信息。例如上述代码中,\(d\) 数组用于记录起点到某个节点的最短距离(要经过的最少边数),\(p\) 数组记录是从哪个节点走到当前节点的。
有了 \(d\) 数组,可以方便地得到起点到一个节点的距离。
有了 \(p\) 数组,可以方便地还原出起点到一个点的最短路径。上述代码中的 restore
函数使用该数组依次输出从起点到节点 \(x\) 的最短路径所经过的节点。
时间复杂度 \(O(n + m)\)
空间复杂度 \(O(n)\)($vis4 数组和队列)
应用
在一个无权图上求从起点到其他所有点的最短路径。
在 \(O(n+m)\) 时间内求出所有连通块。(我们只需要从每个没有被访问过的节点开始做 BFS,显然每次 BFS 会走完一个连通块)
如果把一个游戏的动作看做是状态图上的一条边(一个转移),那么 BFS 可以用来找到在游戏中从一个状态到达另一个状态所需要的最小步骤。
在一个有向无权图中找最小环。(从每个点开始 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[v]+d_b[v]=d_a[b]
,则说明该点在某条最短路上)
找到一条长度为偶数的最短路。(我们需要一个构造一个新图,把每个点拆成两个新点,原图的边 \((u, v)\) 变成 \(((u, 0), (v, 1))\) 和 \(((u, 1), (v, 0))\)。对新图做 BFS,\((s, 0)\) 和 \((t, 0)\) 之间的最短路即为所求)
在一个边权为 \(0/1\) 的图上求最短路,见下方双端队列 BFS。
双向搜索
折半搜索、meet in the middle
过程
Meet in the middle 算法的主要思想是将整个搜索过程分成两半,分别搜索,最后将两半的结果合并。
性质
暴力搜索的复杂度往往是指数级的,而改用 meet in the middle 算法后复杂度的指数可以减半
即让复杂度从 \(O(a^b)\) 降到 \(O(a^{b/2})\)。
思路:搜索,记录当前行数,哪些列被放了,哪些斜对角线被放了。
#include <iostream>
#include <queue>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
#include <cstdio>
#include <set>
#include <map>
#include <unordered_map>
#include <bitset>
using namespace std;
const int maxn = 1e4 + 10, MAXN = 1e9 + 10;
int row_valid[maxn], n, ans =0;
char a[maxn][maxn];
void dfs(int row, int col_ban, int diag_ban_l, int diag_ban_r){
if(row == n){
ans++;
return;
}
int tmp_valid = row_valid[row] & ~ (col_ban | diag_ban_l | diag_ban_r);
for(int i = 0;i < n; i++){
if(tmp_valid >> i & 1){
dfs(row + 1, col_ban | (1 << i),
(diag_ban_l | (1 << i)) << 1,
(diag_ban_r | (1 << i)) >> 1);
}
}
}
int main(){
cin >> n;
for(int i = 0;i < n; i++){
cin >> a[i];
for(int j = 0;j < n; j++){
if(a[i][j] == '*'){
row_valid[i] |= 1 << j;
}
}
}
dfs(0, 0, 0, 0);
cout<< ans << endl;
return 0;
}
标签:24,int,day5,DFS,节点,BFS,vis,include,集训
From: https://www.cnblogs.com/yantaiyzy2024/p/18340817