搜索与图论(一)
DFS 深度优先搜索
842. 排列数字
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 n。
输出格式
按字典序输出所有排列方案,每个方案占一行。
数据范围
1 ≤ n ≤ 7
输入样例:
1 | 3 |
输出样例:
1 | 1 2 3 |
AC
此题采用 DFS 深度优先搜索,也即典型回溯算法。回溯的本质是穷举,但可以加上一些剪枝操作来高效一些。
DFS(回溯算法)解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
DFS 遍历过程
DFS 算法模板框架
1 | void dfs(参数) { |
如何用 dfs 解决全排列
对于全排列问题,以 n = 3 为例,搜索顺序为下:
每次进行到尽头,并回溯走向下一个分支。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using namespace std;
const int N = 10;
int path[N]; // 保存序列
int state[N]; // 数字状态,是否被用过
int n;
void dfs(int u) {
if (u > n) { // 数字填完,排序遍历完成,输出
for (int i = 1; i <= n; i++) {
cout << path[i] << ' ';
}
cout << endl;
}
for (int i = 1; i <= n; i++) { // 空位可选择的数字为:1 ~ n
if (!state[i]) { // 如果数字 i 没有被使用过
path[u] = i; // 放入空位
state[i] = 1; // 修改该数字状态
dfs(u + 1); // 递归,填入下一位数字
state[i] = 0; // 回溯,取出 i
}
}
}
int main() {
cin >> n;
dfs(1);
return 0;
}
1 |
|
843. n-皇后问题
n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数 n。
输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中 .
表示某一个位置的方格状态为空,Q
表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1 ≤ n ≤ 9
输入样例:
1 | 4 |
输出样例:
1 | .Q.. |
AC
思路
皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
- 按行单层搜索,其中
col[x]
,dg[x + y]
,udg[n + y - x]
分别记录的是该位置的列,斜,反斜线上是否已经存在过,若均不存在,填入皇后,并递归到下一行
代码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using namespace std;
const int N = 20;
// bool数组用来判断搜索的下一个位置是否可行
// col列,dg对角线,udg反对角线
// board[N][N]用来存路径
int n;
char board[N][N];
bool col[N], dg[N], udg[N];
void dfs(int u) {
// 判断是否已经搜了 n 行,则打印输出
if (u == n) {
for (int i = 0; i < n; i++) {
cout << board[i] << endl;
}
cout << endl;
return;
}
// 逐行搜索
for (int i = 0; i < n; i++) {
// 剪枝(对于不满足要求的点,不再继续往下搜索)
// udg[n + i - u],+n 是为了保证下标非负
if (!col[i] && !dg[u + i] && !udg[n + i - u]) {
// 满足条件存储
board[u][i] = 'Q';
col[i] = dg[u + i] = udg[n + i - u] = true;
dfs(u + 1); // 递归
// 还原现场
col[i] = dg[u + i] = udg[n + i - u] = false;
board[u][i] = '.';
}
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
dfs(0);
return 0;
}
1 |
|
BFS 广度优先搜索
844. 走迷宫
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
1≤n , m≤100
输入样例:
1 | 5 5 |
输出样例:
1 | 8 |
AC
思路
思路:
用 g[n][m] 存储地图,f[n][m] 存储起点到 n,m 的距离。
从起点开始广度优先遍历地图。
当地图遍历完,就求出了起点到各个点的距离,输出f[n][m] 即可。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using namespace std;
typedef pair<int, int> PII; // 以下标为坐标成对存储
const int N = 110;
int g[N][N]; // 存储地图
int f[N][N]; // 存储距离
int n, m;
void bfs(int a, int b) {
queue<PII> q; // 将坐标按照队列存入
q.push({a, b}); // 存入当前点
while (!q.empty()) { // 队列中有坐标时
PII start = q.front(); // 起点
q.pop(); // 用过的坐标出队列
g[start.first][start.second] = 1; // 走过的防止回头
int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0}; // 四个方向
for (int i = 0; i < 4; i++) {
int x = start.first + dx[i], y = start.second + dy[i]; // 坐标 + 移动方向
if (g[x][y] == 0) { // 如果这条路没有走过
g[x][y] = 1;
f[x][y] = f[start.first][start.second] + 1; // 从当前点走过去,距离等于当前距离+1
q.push({x, y}); // 入队列
}
}
}
cout << f[n][m];
}
int main() {
memset(g, 1, sizeof g); // 初始化地图外圈,即可不用做越界判断
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> g[i][j];
}
}
bfs(1, 1);
return 0;
}
1 |
|
845. 八数码
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x
恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 | 1 2 3 |
在游戏过程中,可以把 x
与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 | 1 2 3 |
例如,示例中图形就可以通过让 x
先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 | 1 2 3 1 2 3 1 2 3 1 2 3 |
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式
输入占一行,将 3×3 的初始网格描绘出来。
例如,如果初始网格如下所示:
1 | 1 2 3 |
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出 −1。
输入样例:
1 | 2 3 4 1 5 x 7 6 8 |
输出样例
1 | 19 |
AC
思路
题目目标
移动情况
移动方式:
将每一种情况作为1个节点,目标情况即为终点
从初始状况移动到目标情况 —> 求最短路
难点
第一点:怎么表示一种情况使其能作为节点?
第二点:如何记录每一个状态的“距离”(即需要移动的次数)?
第三点:队列怎么定义,d 数组怎么定义?
解决方案
将 “3*3矩阵” 转化为 “字符串”
1
2
3
4队列可以用 queue<string>
//直接存转化后的字符串
d数组用 unordered_map<string, int>
//将字符串和数字联系在一起,字符串表示状态,数字表示距离矩阵与字符串的转换方式
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using namespace std;
int bfs(string start) {
string end = "12345678x"; // 目标状态
queue<string> q;
unordered_map<string, int> d;
// 初始化
q.push(start);
d[start] = 0;
int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1}; // 移动方向
while (q.size()) { // 队列不为空时
auto t = q.front(); // 获取队头坐标
q.pop();
int dance = d[t]; // 记录当前距离
if (t == end) { // 如果已经是最终状态,则返回距离
return dance;
}
int k = t.find('x'); // 查询 x 在字符串中的下标
int x = k / 3, y = k % 3; // 转换为矩阵中的坐标
for (int i = 0; i < 4; i++) { // 四个方向移动
int a = x + dx[i], b = y + dy[i]; // 移动后的坐标
if (a >= 0 && a < 3 && b >= 0 && b < 3) { // 若没有出界
swap(t[k], t[a * 3 + b]); // 交换相应位置的数
if (!d.count(t)) { // 若当前状态为第一次遍历,则记录记录,入队
d[t] = dance + 1;
q.push(t);
}
swap(t[k], t[a * 3 + b]); // 恢复现场,为下次做准备
}
}
}
return -1; // 无法转换到目标
}
int main() {
string c, start;
for (int i = 0; i < 9; i++) {
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
1 |
|
树与图的深度优先遍历
846. 树的重心
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围
1 ≤ n ≤ 105
输入样例
1 | 9 |
输出样例:
1 | 4 |
AC
(数组建立邻接表) 树的 dfs
1 | //邻接表 |
树的 bfs 模板
1 | // 需要标记数组st[N], 遍历节点的每个相邻的便 |
本题的本质是树的 dfs, 每次 dfs 可以确定以u为重心的最大连通块的节点数,并且更新一下 ans。
也就是说,dfs 并不直接返回答案,而是在每次更新中迭代一次答案。
这样的套路会经常用到,在 树的 dfs 题目中
代码
1 |
|
树与图的广度优先遍历
847. 图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 11 号点无法走到 n 号点,输出 −1。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
数据范围
1 ≤ n,m ≤ 105
输入样例:
1 | 4 5 |
输出样例:
1 | 1 |
AC
思路
用 d 数组保存1号节点到各个节点的距离,初始时,都是无穷大。
用 st 数组标记各个节点有没有走到过。
从 1 号节点开始,广度优先遍历:
1 号节点入队列,d[1] 的值更新为 0。
如果队列非空,就取出队头,找到队头节点能到的所有节点。如果队头节点能到走到的节点没有标记过,就将节点的d值更新为队头的d值+1,然后入队。
重复步骤 2 直到队列为空。
这个时候,d数组中就存储了 1 号节点到各个节点的距离了。如果距离是无穷大,则不能到达,输出 -1,如果距离不是无穷大,则能到达,输出距离。
图的存储:邻接表
用 h 数组保存各个节点能到的第一个节点的编号。开始时,h[i] 全部为 -1。
用 e 数组保存节点编号,ne 数组保存 e 数组对应位置的下一个节点所在的索引。
用 idx 保存下一个 e 数组中,可以放入节点位置的索引
插入边使用的头插法,例如插入:a->b。首先把b节点存入e数组,e[idx] = b。然后 b 节点的后继是h[a],ne[idx] = h[a]。最后,a 的后继更新为 b 节点的编号,h[a] = idx,索引指向下一个可以存储节点的位置,idx ++ 。
代码
1 |
|