深度优先搜索算法练习
*本篇文章涉及较多知识点,可以在下面查看对应的教程:
递归基础 / 递归进阶 / 递归练习1 / 递归练习2 / DFS1 / DFS2 / 回溯算法 / DFS3
一、递归
1. 变化的数
有一个数 a a a,想把这个数变成 b b b,为此可以做两种变换:
① x x x 变为 2 x 2x 2x
② x x x 变为 10 x + 1 10x+1 10x+1
例如:2 -> 4 -> 8 -> 81 -> 162
你需要判断一下,把
a
a
a 变成
b
b
b 的是否可能,可能则输出 YES
,否则输出 NO
。
#include <iostream>
using namespace std;
// 递归判断是否存在变换路径
bool transform(int a, int b)
{
if (a == b) return 1; // 当 a 和 b 相等时,变换成功,返回 true
if (a > b) return 0; // 当 a 大于 b 时,无法变换,返回 false
return transform(a*2, b) || transform(a*10+1, b); // 递归进行两种变换判断
}
int main()
{
int a, b;
cin >> a >> b;
cout << (transform(a, b) ? "YES" : "NO"); // 输出是否存在变换路径
return 0;
}
2. 数字分解
一个正整数,可以分解成多个大于等于 1 1 1 的整数之和的形式,要求这些数字从左向右是递增的(即后一个数小于等于前一个数)。请你求出对于一个整数 n n n,一共有多少种分解方案。
#include <iostream>
using namespace std;
int n;
int f(int n, int maxn)
{
if (n == 0) return 1;
int cnt = 0;
for (int i = 1; i <= maxn && i <= n; i++)
{
cnt += f(n-i, i);
}
return cnt;
}
int main()
{
cin >> n;
cout << f(n, n);
}
二、DFS
1. 八个方向的迷宫
给定一个 n n n 行 m m m 列的迷宫,有些格子可以走,有些有障碍物不能到达。每步可以走到周围 8 8 8 个方向的格子中。请你判断,是否能从左上角走到右下角。如果能走到输出
YES
,否则输出NO
。
同样地,不撞南墙不回头,记得将偏差值改一下就好了。
#include <iostream>
#include <cstdio>
using namespace std;
int n, m; // 迷宫大小
bool flag; // 是否有解
char Map[25][25]; // 地形图
bool vis[25][25]; // 标记是否走过
int dx[10] = {-1, -1, -1, 0, 0, 1, 1, 1}; // 八个方向的偏移量
int dy[10] = {-1, 0, 1, -1, 1, -1, 0, 1}; // 八个方向的偏移量
void dfs(int x, int y)
{
// 到终点
if (x == n && y == m)
{
flag = true;
return;
}
// 遍历方向,判断是否满足条件
for (int i = 0; i < 8; i++)
{
int tmpX = x + dx[i];
int tmpY = y + dy[i];
// 是通路
if (Map[tmpX][tmpY] == '.')
{
// 未到边界
if (tmpX >= 1 && tmpX <= n && tmpY >= 1 && tmpY <= m)
{
// 未访问
if (vis[tmpX][tmpY] == false)
{
vis[tmpX][tmpY] = true;
dfs(tmpX, tmpY);
}
}
}
}
}
int main()
{
freopen("map.in", "r", stdin);
freopen("map.out", "w", stdout);
// 输入
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cin >> Map[i][j];
}
}
// dfs
vis[1][1] = 1;
dfs(1, 1);
// 输出
cout << (flag ? "YES" : "NO");
fclose(stdin);
fclose(stdout);
return 0;
}
2. n 皇后
题目描述
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n n n 皇后问题研究的是如何将 n n n 个皇后放置在 n × n n\times n n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n n n,求出所有不同的 n n n 皇后问题的解决方案。
每一种解法包含一个不同的 n n n 皇后问题的棋子放置方案,该方案中'Q'
和'.'
分别代表了皇后和空位。
输入描述
仅一行,一个正整数 n n n。
输出描述
若干行, n × n n\times n n×n 的棋盘所有的解,每个解用空格隔开
样例1
输入
4
输出
.Q.. ...Q Q... ..Q. ..Q. Q... ...Q .Q..
提示
4 ≤ n ≤ 18 4≤n≤18 4≤n≤18
思路:这是根据《C++知识点总结(37):回溯算法》中第四节的题目改编而来。我们可以知道,按照题目所描述的方法,我们应该使得:
- 每一行都只有一个皇后
- 每一列都只有一个皇后
- 每一个正对角线都只有一个皇后
- 每一个负对角线都只有一个皇后
#include <iostream>
using namespace std;
int n;
bool a[20][20];
bool row[20];
bool col[20];
bool zd[40], fd[40];
void dfs(int r)
{
if (r > n)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (a[i][j])
{
cout << "Q";
}
else
{
cout << ".";
}
}
cout << endl;
}
cout << endl;
return;
}
for (int j = 1; j <= n; j++) // 遍历每个列
{
if (!a[r][j] && !row[r] && !col[j] && !zd[r+j] && !fd[r-j+n]) // 是否没有冲突
{
a[r][j] = true;
row[r] = true;
col[j] = true;
zd[r+j] = true;
fd[r-j+n] = true;
dfs(r+1);
a[r][j] = false;
row[r] = false;
col[j] = false;
zd[r+j] = false;
fd[r-j+n] = false;
}
}
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
3. 玩具蛇
题目描述
小蓝有一条玩具蛇,一共有 n 2 n^2 n2 节,上面标着数字1至 n 2 n^2 n2,每一节都是一个正方形的形状,相邻的两节可以成直线或者成直角。
小蓝还有一个 n × n n\times n n×n 的方格盒子,用于存放玩具蛇,盒子的方格上依次标着字母共 n 2 n^2 n2 个字母。
小蓝可以折叠自己的玩具蛇放到盒子里面。他发现,有很多种方案可以将玩具蛇放进去。
请帮小蓝计算一下,总共有多少种不同的方案。如果存在玩具蛇的某一节放在了盒子的不同格子里,则认为是不同的方案。
输入描述
一个整数 q q q,表示输入 q q q 组方格盒子。
接下来 q q q 行,每行代表一个 n × n n\times n n×n 的方格盒子。
输出描述
q q q 个整数表示每个方格盒子的方案数。
样例1
输入
2 2 4
输出
8 552
提示
1 ≤ n ≤ 5 1\le n\le5 1≤n≤5
#include <iostream>
#include <cstring>
using namespace std;
int q; // 输入的组数
int n; // 盒子的边长
bool vis[10][10]; // 盒子是否被占据
int total; // 方案数量
int dx[5] = {-1, 0, 1, 0};
int dy[5] = {0, 1, 0, -1};
void dfs(int x, int y, int cnt)
{
if (cnt == n*n)
{
total++;
return;
}
for (int i = 0; i < 4; i++)
{
int tx = x+dx[i];
int ty = y+dy[i];
if (tx>=1 && tx<=n && ty>=1 && ty<=n)
{
if (vis[tx][ty] == 0)
{
vis[tx][ty] = 1;
dfs(tx, ty, cnt+1);
vis[tx][ty] = 0;
}
}
}
}
int main()
{
// 输入
cin >> q;
for (int i = 1; i <= q; i++)
{
cin >> n;
total = 0;
memset(vis, 0, sizeof(vis));
// dfs
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
vis[i][j] = 1;
dfs(i, j, 1);
vis[i][j] = 0;
}
}
// 输出
cout << total << endl;
}
return 0;
}
4. 深度优先搜索顺序
题目描述
给定一个 n n n 行 m m m 列的迷宫,有些格子可以走,有些有障碍物不能到达。每步可以走到上下左右的格子中。请你输出从左上角(第一行第一列)开始深度优先搜索所有格子的顺序。其中,从一个格子出发,优先出发的顺序为:上、下、左、右,每个格子只遍历一次,即按搜索顺序输出从左上角开始能够搜索到的所有位置。
输入描述
第一行有两个正整数 n n n 和 m m m,表示迷宫的行数和列数。
接下来 n n n 行为输入这个迷宫,每行为一个长度为 m m m 的字符串。第 i i i 行第 j j j 列的字符为'*'
表示迷宫第 i i i 行第 j j j 列的格子有障碍物,为'.'
表示没有障碍物。
输出描述
若干行,按遍历顺序在每行输出搜索到的格子。第 x x x 行第 y y y 列的格子以
"(x,y)"
的格式输出。
样例1
输入
3 3 .*. ... ***
输出
(1,1) (2,1) (2,2) (2,3) (1,3)
提示
1 ≤ n , m ≤ 100 1 \le n, m \le 100 1≤n,m≤100
#include <iostream>
using namespace std;
int n, m;
char a[105][105];
bool vis[105][105];
int dx[5] = {-1, 1, 0, 0};
int dy[5] = {0, 0, -1, 1};
int pos = 1;
int ansx[10005];
int ansy[10005];
void dfs(int x, int y)
{
cout << "(" << x << "," << y << ")\n";
for (int i = 0; i <= 3; i++)
{
int tx = x+dx[i];
int ty = y+dy[i];
if (a[tx][ty] == '.')
{
if(tx>=1 && tx<=n && ty>=1 && ty<=m)
{
if (vis[tx][ty] == 0)
{
vis[tx][ty] = 1;
dfs(tx,ty);
}
}
}
}
}
int main()
{
// 输入
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cin >> a[i][j];
}
}
// dfs
vis[1][1] = 1;
dfs(1, 1);
return 0;
}
5. 单词消消乐
题目描述
给定一个 n × n n \times n n×n 的字母方阵和一个字符串单词,内可能含有多个这样的单词,单词在方阵中是沿着同一方向连续摆放的,摆放可沿着 8 8 8 个方向的任一方向,同一单词摆放时不再改变单词方向,单词与单词之间可以交叉,因此可能共用字母,输出时,将不是单词的字母用
'*'
代替,以凸显单词。
输入描述
输入文件名word.in。
第一行一个数 n n n,表示字母方阵的大小( 7 ≤ n ≤ 100 7 \le n \le 100 7≤n≤100),第二行开始输入 n × n n \times n n×n 的字母方阵,字母间没有空格。
最后一行输入一个字符串单词,单词长度在 1 1 1 到 15 15 15 之间。
输出描述
输出文件名word.out。
突出显示单词的 n × n n \times n n×n 字母方阵。
样例1
输入
4 abcd fesh eawj qbso ab
输出
ab** **** *a** *b**
提示
题中所有字母均为小写
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
using namespace std;
int n;
int pos;
char a[120][120];
int dx[10] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[10] = {-1, 0, 1, -1, 1, -1, 0, 1};
string s;
int px[120], py[120];
bool vis[120][120];
void dfs(int x, int y, int h)
{
if (pos == s.length()) // 边界
{
for (int i = 0; i < pos; i++)
{
vis[px[i]][py[i]] = 1;
}
return;
}
int tx = x + dx[h];
int ty = y + dy[h];
if (tx >= 1 && tx <= n && ty >= 1 && ty <= n && a[tx][ty] == s[pos])
{
px[pos] = tx;
py[pos] = ty;
pos++;
dfs(tx, ty, h);
}
}
int main()
{
freopen("word.in", "r", stdin);
freopen("word.out", "w", stdout);
// 输入
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> a[i][j];
}
}
cin >> s;
// 找到开头字符
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (a[i][j] == s[0])
{
for (int h = 0; h <= 7; h++)
{
pos = 0;
// memset(px, 0, sizeof(px));
// memset(py, 0, sizeof(py));
px[0] = i;
py[0] = j;
pos++;
dfs(i, j, h);
}
}
}
}
// 输出
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (vis[i][j])
{
cout << a[i][j];
}
else
{
cout << "*";
}
}
cout << endl;
}
fclose(stdin);
fclose(stdout);
return 0;
}
6. 奇怪的系统
题目描述
原本平静的生活被打破,你被卷入一场神秘的案件中,成为侦探团的一员,由于你自带解谜系统,所以解决案件对你来说小菜一碟,但系统有一个神奇的地方,只有给出满足要求的案件物品,系统才能给出线索,每个案件物品都有线索值,因此组合得到的线索也不一样。
现在你面前有 n n n 个不同线索值的案件物品,系统需要的线索值为 m m m,每给出一组不同的物品组合使线索值总和刚好为 m m m 则可以得到一条新的线索,并且物品不会因为给系统而消失,为了解开这个谜团,你需要选择合适数量与线索值的案件物品给系统,以此得到不同的线索,你一共能够得到多少条线索呢?
输入描述
第一行输入 n n n 和 m m m,第二行一共 n n n 个数字,表示每个案件物品的线索值 a i a_i ai。
输出描述
输出能够得到的线索条数。
样例1
输入
3 40 20 20 20
输出
3
提示
【样例解释】
一共 3 3 3 个物品,线索值分别为 20 , 20 , 20 20,20,20 20,20,20
系统需要的线索值为 40 40 40,则可以 1 , 2 1,2 1,2 组合, 1 , 3 1,3 1,3 组合, 2 , 3 2,3 2,3 组合刚好都为 40 40 40, 1 , 2 , 3 1,2,3 1,2,3 组合超过 40 40 40 不符合,所以得到 3 3 3 条线索。
【数据范围】
0 < m ≤ 100 , 1 ≤ n ≤ 20 , a i ≤ m 0<m\le100,1\le n\le20,a_i\le m 0<m≤100,1≤n≤20,ai≤m
#include <iostream>
using namespace std;
int n, m;
int cnt;
int a[25];
bool used[25];
// 判断选择的数字之和是否等于预设值
bool check()
{
// 求和
int sum = 0;
for (int i = 1; i <= n; i++)
{
if (used[i])
{
sum += a[i];
}
}
// 判断是否等于预设值
if (sum == m)
{
return true;
}
return false;
}
// pos: 当前位置
// cnt: 选的数字个数
void dfs(int pos)
{
if (pos > n) // 边界
{
if (check()) // 题目条件
{
cnt++;
}
return;
}
dfs(pos+1); // 不选
used[pos] = 1; // 标记
dfs(pos+1); // 选
used[pos] = 0; // 回溯
}
int main()
{
// 输入
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
// dfs
dfs(1);
// 输出
cout << cnt;
return 0;
}
7. [USACO23JAN] Air Cownditioning II B
#include <iostream>
using namespace std;
int n, m, k;
int ans = 1e9;
int cw[105], s[25], t[25], c[25], a[25], b[25], p[25], v[25];
bool check() // 是否满足条件
{
for(int i = 1; i <= k; i++)
{
if (cw[i] > 0)
{
return false;
}
}
return true;
}
void dfs(int pos, int s)
{
if (pos > m)
{
if (check())
{
ans = min(ans, s);//满足条件,更新答案
}
return;
}
dfs(pos+1, s); // 不选
// 标记
for(int i = a[pos]; i <= b[pos]; i++)
cw[i] -= p[pos];
dfs(pos+1, s+v[pos]); // 选
// 回溯
for(int i = a[pos]; i <= b[pos]; i++)
cw[i] += p[pos];
}
int main()
{
// 输入
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> s[i] >> t[i] >> c[i];
k = max(k, t[i]);
for (int j = s[i]; j <= t[i]; j++)
{
cw[j] += c[i]; //事先处理
}
}
for (int i = 1; i <= m; i++)
cin >> a[i] >> b[i] >> p[i] >> v[i];
// dfs
dfs(1, 0);
// 输出
cout << ans;
return 0;
}
三、排列组合
选择同学
题目描述
今有 n n n 位同学,可以从中选出任意名同学参加合唱。
请输出所有可能的选择方案。
输入描述
仅一行,一个正整数 n n n。
输出描述
若干行,每行表示一个选择方案。
每一种选择方案用一个字符串表示,其中第 i i i 位为 Y Y Y 则表示第 i i i 名同学参加合唱;为 N N N 则表示不参加。
样例1
输入
3
输出
NNN NNY NYN NYY YNN YNY YYN YYY
提示
1 ≤ n ≤ 10 1≤n≤10 1≤n≤10
思路:这是非常典型的一道回溯题,与《C++知识点总结(37):回溯算法》中第二节的第一小节比较类似。因为这道题目只有
Y
Y
Y 和
N
N
N 两种可能,所以在深度优先搜索的时候,我们只需要用两个递归就可以,不需要 for
循环了。
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
int n;
string s;
void dfs(int pos, string s)
{
if (pos > n)
{
cout << s << endl;
return;
}
dfs(pos+1, s+'N');
dfs(pos+1, s+'Y');
}
int main()
{
cin >> n;
dfs(1, s);
return 0;
}
四、剪枝优化
1. 走迷宫
题目描述
在一个神秘的迷宫中,有一个勇敢的冒险家,他的目标是从起点 ( 1 , 1 ) (1,1) (1,1) 走到终点 ( n , n ) (n,n) (n,n)。但是,这个迷宫并不是那么容易通过的,有些地方是可以走的,有些地方是恶龙所在的区域。为了成功通过迷宫,冒险家必须遵循以下规则:
- 只能向下、右两个方向移动;
- 在移动过程中,可以至多转向 k k k 次。
如果一条路线中冒险家经过了某个方格而另一条路线中没有,则认为这两条路线不同。
现在,你需要帮助这位勇敢的冒险家,计算他从起点到终点的方案数。
输入描述
输入文件名maze.in。
第一行包含三个整数 n , k , m n,k,m n,k,m,表示矩阵的大小,转向次数和障碍物的数量。
接下来 m m m 行,每行包含两个整数 x x x 和 y y y,表示第 x x x 行第 y y y 列是障碍物。
输出描述
输出文件名maze.out。
输出一个整数,表示从起点到终点的方案数,方案数可能为 0 0 0(那就算给恶龙饱餐一顿了)。
样例1
输入
5 2 2 2 2 3 4
输出
4
提示
1 ≤ n ≤ 50 1 \leq n \leq 50 1≤n≤50
1 ≤ k ≤ 5 1 \leq k \leq 5 1≤k≤5
1 ≤ m ≤ 100 1 \leq m \leq 100 1≤m≤100
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n, k, m;
int cnt;
bool a[60][60];
int dx[5] = {0, 1};
int dy[5] = {1, 0};
void dfs(int x, int y, int turn, int direc)
{
if (x == n && y == n) // 到达终点
{
if (turn <= k)
{
cnt++;
}
return;
}
// 剪枝
if (turn > k)
{
return;
}
if (turn == k)
{
if (x != n && y != n)
{
return;
}
}
// 递归
for (int i = 0; i <= 1; i++)
{
int tx = x + dx[i];
int ty = y + dy[i];
// 是通路
if (a[tx][ty] == 1)
{
// 未到边界
if (tx >= 1 && tx <= n && ty >= 1 && ty <= n)
{
// 是否转弯
if (i != direc && direc != -1)
{
dfs(tx, ty, turn+1, i);
}
else
{
dfs(tx, ty, turn, i);
}
}
}
}
}
int main()
{
freopen("maze.in", "r", stdin);
freopen("maze.out", "w", stdout);
// 初始化迷宫(默认都是通路)
memset(a, 1, sizeof(a));
// 输入
cin >> n >> k >> m;
for (int i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
a[x][y] = 0;
}
// dfs
dfs(1, 1, 0, -1);
cout << cnt;
fclose(stdin);
fclose(stdout);
return 0;
}
2. 危险的工作
题目描述
在一个神秘的岛屿上,有 N N N 个勇士需要分担 N N N 个危险的工作。每个勇士都有自己的特长,可以担任其中一项工作。每个工作都有一个危险值,代表完成这项工作的风险程度。每个勇士担任工作时,会承担该工作的危险值。现在,你需要为这些勇士分配工作,使得每个勇士承担的危险值之和最小。
输入描述
第一行输入一个整数 N N N,代表勇士的数量。
接下来 N N N 行,每行输入 N N N 个整数,第 i i i 行的 N N N 个数字分别代表第 i i i 个勇士担任 1 − N 1-N 1−N 项工作的危险值,用空格隔开。
输出描述
输出一个整数,代表每个勇士承担的危险值之和的最小值。
样例1
输入
3 1 2 3 4 5 6 7 8 9
输出
15
提示
1 ≤ N ≤ 11 1 \le N \le 11 1≤N≤11
1 ≤ 1 \le 1≤ 危险值 ≤ 100 \le 100 ≤100
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
int d[20][20];
int mind = 1e8;
int a[20];
bool vis[20];
void dfs(int pos, int total)
{
if (pos > n) // 边界
{
mind = min(mind, total);
return;
}
for (int i = 1; i <= n; i++)
{
if (!vis[i]) // 检查位置是否被占用
{
vis[i] = true;
a[pos] = i; // 标记
int new_total = total + d[pos][i];
if (new_total <= mind) // 剪枝
dfs(pos + 1, new_total); // 递归下一个位置
a[pos] = -1; // 回溯
vis[i] = false;
}
}
}
int main()
{
// 初始化
memset(a, -1, sizeof(a));
memset(vis, false, sizeof(vis));
// 输入
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> d[i][j];
// dfs
dfs(1, 0);
// 输出
cout << mind;
return 0;
}
3. 规定时间走迷宫
第一行给定迷宫的大小 n , m n,m n,m 和规定时间 t t t,表示有一个 n × m n\times m n×m 的迷宫,起点到终点正好是规定时间(每向某个方向走一格,就算 1 s 1s 1s)。
接下来 n n n 行每行 m m m 个空格隔开的字符,'.'
代表空地,'*'
代表障碍。
最后一行给定初始位置和终点位置 s x , s y , e x , e y sx,sy,ex,ey sx,sy,ex,ey。
求你求出符合规定时间的路径数。
数据范围: 1 ≤ n , m ≤ 100 1\le n,m \le 100 1≤n,m≤100
#include <iostream>
#include <cmath>
using namespace std;
int n, m, t, sx, sy, ex, ey, cnt;
char a[105][105];
bool vis[105][105];
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
void dfs(int x, int y, int l)
{
// 剪枝
if (abs(ex-x)+abs(ey-y) > t-l || l>t)
return;
// 边界
if (x==ex && y==ey && l==t)
{
cnt++;
return;
}
for (int i = 0; i <= 3; i++)
{
int nx = x+dx[i];
int ny = y+dy[i];
if (nx>=0 && nx<=n && ny>=0 && ny<=m) // 未到边界
{
if (vis[nx][ny]==0 && a[nx][ny]=='.') // 未访问、是空地
{
vis[nx][ny] == 1;
dfs(nx, ny, l+1);
vis[nx][ny] == 0;
}
}
}
}
int main()
{
// 输入
cin >> n >> m >> t;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cin >> a[i][j];
}
}
cin >> sx >> sy >> ex >> ey;
// dfs
dfs(sx, sy, 0);
// 输出
cout << cnt;
return 0;
}