深度优先搜索算法练习
- 一、递归
- 1. 变化的数
- 2. 数字分解
- 二、DFS
- 1. 八个方向的迷宫
- 2. n 皇后
- 3. 玩具蛇
- 4. 深度优先搜索顺序
- 5. 单词消消乐
- 6. 奇怪的系统
- 7. [USACO23JAN] Air Cownditioning II B
- 三、排列组合
- 选择同学
- 四、剪枝优化
- 1. 走迷宫
- 2. 危险的工作
- 3. 规定时间走迷宫
*本篇文章涉及较多知识点,可以在下面查看对应的教程:
递归基础 / 递归进阶 / 递归练习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 相等时,变换成功,返回 trueif (a > b) return 0; // 当 a 大于 b 时,无法变换,返回 falsereturn 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];}}// dfsvis[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));// dfsfor (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];}}// dfsvis[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];}// dfsdfs(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];// dfsdfs(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;}// dfsdfs(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];// dfsdfs(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;// dfsdfs(sx, sy, 0);// 输出cout << cnt;return 0;
}