3.1 搜索与图论 | DFS、BFS、拓扑排序
这是我的一个算法网课学习记录,道阻且长,好好努力
3.1.1 DFS与BFS
DFS与BFS都可以对空间进行搜索,搜索的结构像一棵树一样,但是搜索的方式不同。
DFS十分“执着”,不撞南墙不回头;BFS比较“稳重”,层层递进。
一些比较:
数据结构 | 空间 | 特性 | |
---|---|---|---|
DFS | 栈stack | O(h) | 空间有优势 |
BFS | 队列queue | O(2h) | 最短路 |
3.1.1.1 深度优先搜索 DFS
暴力搜索
从搜索树的角度思考
例题1:ACWing 842. 排列数字
给定一个正整数n,生成1 ∼ n 的全排列,按照字典序输出。
#include <iostream>
using namespace std;
const int N = 10;
int n;
int path[N];
bool st[N];
void dfs(int u)
{
// 如果走到路径的尽头,返回路径
if (u == n)
{
for (int i = 0; i < n; i ++ ) printf("%d ", path[i]);
puts("");
return;
}
for (int i = 1; i <= n; i ++ ) // 循环,进行枚举
if (!st[i]) // 如果这个数没有被使用过
{
path[u] = i; // 存入路径节点
st[i] = true; // 更新标记
dfs(u + 1); // 进入下一层递归
st[i] = false; // 回溯:恢复现场
}
}
int main()
{
cin >> n;
dfs(0);
return 0;
}
对于一些问题可以全部遍历一遍,在逐一判断是否符合要求;也可以在遍历的时候进行判断,从而减少一定的循环次数,也就是所谓的 剪枝 (剪枝有很多中方法)
例题2:ACWing 843. n-皇后问题
n-皇后问题是指将 n 个皇后放在 n∗n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
Ans_1:
#include <iostream>
using namespace std;
const int N = 10;
int n;
char g[N][N];
bool col[N], dg[N], udg[N]; // 分别用于判断是否在同一列、正对角线、反对角线
void dfs(int u)
{
if (u == n)
{
for (int i = 0; i < n; i ++ ) puts(g[i]); // 逐行输出
puts("");
return;
}
for (int i = 0; i < n; i ++ ) // 循环,进行枚举
{
if (!col[i] && !dg[u + i] && !udg[n - u + i]) // 如果满足规则
{
g[u][i] = 'Q'; // 将皇后存入该点
col[i] = dg[u + i] = udg[n - u + i] = true; // 更新标记
dfs(u + 1); // 进入下一层递归
col[i] = dg[u + i] = udg[n - u + i] = false; // 回溯:还原
g[u][i] = '.'; // 覆盖皇后
}
}
}
int main() {
cin >> n;
// 初始化棋盘
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
g[i][j] = '.';
dfs(0);
return 0;
}
通过分析可以知道,每一行只会有一个皇后,再运用dfs
。
22行的代码if (!col[i] && !dg[u + i] && !udg[n - u + i])
由于在坐标系当中,在这里的情景下,直线的关系有两种y = x + b
与y = - x + b
,因此有b = x - y
与b = x + y
,对于前者,由于数组的idx
不能是负数,因此加上一个n
,修正使之为正数。于是,在这里在判断正反对角线的时候,就有了udg[n - u + i]
加上了一个n
。
Ans_2:更原始的做法
#include <iostream>
using namespace std;
const int N = 10;
int n;
int path[N];
bool st[N];
void dfs(int u)
{
// 如果走到路径的尽头,返回路径
if (u == n)
{
for (int i = 0; i < n; i ++ ) printf("%d ", path[i]);
puts("");
return;
}
for (int i = 1; i <= n; i ++ ) // 循环,进行枚举
if (!st[i]) // 如果这个数没有被使用过
{
path[u] = i; // 存入路径节点
st[i] = true; // 更新标记
dfs(u + 1); // 进入下一层递归
st[i] = false; // 回溯:恢复现场
}
}
int main()
{
cin >> n;
dfs(0);
return 0;
}
3.1.1.2 宽度优先搜索 BFS
例题:ACWing 844. 走迷宫
给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N]; // 存放地图
int d[N][N]; // 存放每个点到起点的距离
PII q[N * N]; // 手写的队列
PII Prev[N][N]; // 存放每个点是由其前一个点扩展出来的
int bfs()
{
int hh = 0, tt = 0; // 队列 初始化
q[0] = {0, 0};
memset(d, -1, sizeof d); // 距离初始化为-1 表示没有走过
d[0][0] = 0; // 表示起点走过了
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 声明方向向量
while (hh <= tt) // 队列不空
{
auto t = q[hh ++ ]; // 取队头元素
for (int i = 0; i < 4; i ++ ) // 枚举4个方向
{
int x = t.first + dx[i], y = t.second + dy[i]; //
if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
{
d[x][y] = d[t.first][t.second] + 1; // 更新到起点的距离
Prev[x][y] = t;
q[ ++ tt] = {x, y}; // 新坐标入队
}
}
}
int x = n - 1, y = m - 1;
while (x || y) // x和y只要有一个不等于0 输出从终点到起点的路线坐标
{
cout << x << ' ' << y << endl;
auto t = Prev[x][y];
x = t.first, y = t.second;
}
return d[n - 1][m - 1]; // 输出右下角点距离起点的距离
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
3.1.1.3 树与图的存储
树是一种特殊的图 (无环连通图)。
图分为两种,有向图(边是有方向的)与无向图(边是无方向的,即a–>b, a<–b)。
- 有向图
- 邻接矩阵
- 邻接表:有n个点,就开n个单链表,用于存储该点可以到达哪些点。(类比拉链法)
3.1.1.4 树与图的深度优先遍历
例题:ACWing 846. 树的重心
给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = N * 2; // M是双向边 *2
int n, m;
int h[N], e[M], ne[M], idx;
bool st[N];
int ans = N;
void add(int a, int b) // 将b添加到a节点指向的数的链表中
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 返回以u为根的子树中点的数量
int dfs(int u)
{
st[u] = true; // 标记一下u点已经被搜过了
int sum = 1, res = 0;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) // 如果没有被标记
{
int s = dfs(j);
res = max(res, s);
sum += s;
}
}
res = max(res, n - sum); // 删去后该节点后,剩余各个连通块中点数的最大值
ans = min(ans, res);
return sum;
}
int main()
{
cin >> n;
memset(h, -1, sizeof h); // 初始化h数组元素为-1
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
cin >> a >> b;
add(a, b), add(a, b); // 双向标记
}
dfs(1);
cout << ans << endl;
return 0;
}
3.1.1.5 树与图的宽度优先遍历
重边是指有两条完全相同的边,自环是指自己指向自己。
例题:ACWing 847. 图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 − 1。
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx; // 邻接表
int d[N], q[N]; // 数组模拟队列 d是距离 q是队列
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int bfs()
{
int hh = 0, tt = 0;
q[0] = 1;
memset(d, -1, sizeof d); // 初始化距离为-1
d[1] = 0;
while (hh <= tt)
{
int t = q[hh ++ ]; // 取队头
for (int i = h[t]; i != -1; i = ne[i]) // 对每一层的节点进行遍历
{
int j = e[i];
if (d[j] == -1) // 如果没有被搜索到
{
d[j] = d[t] + 1; // 当前节点到根节点的距离+1
q[ ++ tt] = j; // 插入到队列当中
}
}
}
return d[n]; // 返回n点到根节点的距离
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
cin >> a >> b;
add(a, b);
}
cout << bfs() << endl;
return 0;
}
3.1.2 树与图的遍历拓扑排序
图的拓扑序必须要满足以下两点:
- 每个顶点只出现一次。
- 对于图中的任何一条边,起点必须在终点之前。
有向图才有拓扑序列。
一个有向无环图一定存在一个拓扑序列。
一个有向无环图一定至少存在一个入度为0的点。
一个有向无环图的拓扑序列不一定唯一。
例题:ACWing 848. 有向图的拓扑序列
给定一个n个点m条边的有向图,点的编号是1到n,图中可能存在重边和自环。输出其任意一个拓扑序列。如果不存在则返回-1 。
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N]; // q表示队列 d表示入度
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool toposort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if(!d[i]) // 如果入度为0 进队
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
d[j] -- ; // 删掉前面的一个点 入度-1
if (d[j] == 0) q[ ++ tt] = j;
}
}
return tt == n - 1;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
cin >> a >> b;
add(a, b);
d[b] ++ ;
}
if (toposort())
{
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
puts("");
}
else puts("-1");
return 0;
}