24暑假集训day5下午
DFS
本质:一种用于遍历或搜索树或图的算法。所谓深度优先,就是说每次都尝试向更深的节点走。
该算法讲解时常常与 BFS 并列,但两者除了都能遍历图的连通块以外,用途完全不同,很少有能混用两种算法的情况。
关键:
递归调用自身
对其访问过的点打上访问标记,在遍历图时跳过已打过标记的点,以确保 每个点仅访问一次。
伪代码:
DFS(v) // v 可以是图中的一个顶点,也可以是抽象的概念,如 dp 状态等。
在 v 上打访问标记
for u in v 的相邻节点
if u 没有打过访问标记 then
DFS(u)
end
end
end
性质
该算法通常的时间复杂度为
实现
用栈(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 进入某个节点的时候记录一个左括号 (
,退出某个节点的时候记录一个右括号 )。
每个节点会出现两次。相邻两个节点的深度相差
一般图上 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("");
}
具体来说,我们用一个队列 vis[]
来标记是否已经访问过某个节点。
开始的时候,我们将所有节点的 vis[s]
设为
之后,我们每次从队列
循环直至当队列
在 BFS 的过程中,也可以记录一些额外的信息。例如上述代码中,
有了
有了 restore
函数使用该数组依次输出从起点到节点
时间复杂度
空间复杂度
应用
在一个无权图上求从起点到其他所有点的最短路径。
在
如果把一个游戏的动作看做是状态图上的一条边(一个转移),那么 BFS 可以用来找到在游戏中从一个状态到达另一个状态所需要的最小步骤。
在一个有向无权图中找最小环。(从每个点开始 BFS,在我们即将抵达一个之前访问过的点开始的时候,就知道遇到了一个环。图的最小环是每次 BFS 得到的最小环的平均值。)
找到可能在 d_a[u]+1+d_b[v]=d_a[b]
,则说明该边在最短路上)
找到可能在 d_a[v]+d_b[v]=d_a[b]
,则说明该点在某条最短路上)
找到一条长度为偶数的最短路。(我们需要一个构造一个新图,把每个点拆成两个新点,原图的边
在一个边权为
双向搜索
折半搜索、meet in the middle
过程
Meet in the middle 算法的主要思想是将整个搜索过程分成两半,分别搜索,最后将两半的结果合并。
性质
暴力搜索的复杂度往往是指数级的,而改用 meet in the middle 算法后复杂度的指数可以减半
即让复杂度从
思路:搜索,记录当前行数,哪些列被放了,哪些斜对角线被放了。
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)