DFS基本应用和拓展
一、DFS之连通性模型
上一章BFS也有连通性的模型,会比DFS快一些,因为BFS是扩散类型的记录全图,而DFS则是“一条路走到黑”型的记录。
例题1、Acwing 1112. 迷宫
本题解析主要说一下“一条路走到黑”的具体含义,如下图所示:
其中成功道路归入答案集,失败道路不予考虑。
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int T, n;
char g[N][N];
int sx, sy, ex, ey;
bool st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool dfs(int x, int y) {
if (g[x][y] == '#') return false;
st[x][y] = true;
if (x == ex && y == ey) return true;
for (int i = 0; i < 4; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= n) continue;
if (st[a][b]) continue;
// 递归判断
if (dfs(a, b)) return true;
}
return false;
}
int main() {
scanf("%d", &T);
while (T--) {
memset(st, 0, sizeof st);
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%s", g[i]);
scanf("%d%d%d%d", &sx, &sy, &ex, &ey);
if (dfs(sx, sy)) puts("YES");
else puts("NO");
}
return 0;
}
本题并不是找到路,与BFS的扩散记录全图的题型很相似,时间会有一些慢,思路都是一样的。
DFS版本代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 30;
int n, m, res;
char g[N][N];
bool st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void dfs(int x, int y) {
st[x][y] = true;
res++;
for (int i = 0; i < 4; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (st[a][b] || g[a][b] == '#') continue;
dfs(a, b);
}
}
int main() {
while (cin >> m >> n, n || m) {
memset(st, false, sizeof st);
for (int i = 0; i < n; i++) cin >> g[i];
int x, y;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (g[i][j] == '@') {
x = i, y = j;
break;
}
}
}
res = 0;
dfs(x, y);
cout << res << endl;
}
return 0;
}
BFS代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 30;
int n, m, res;
char g[N][N];
bool st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void dfs(int x, int y) {
st[x][y] = true;
res++;
for (int i = 0; i < 4; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (st[a][b] || g[a][b] == '#') continue;
dfs(a, b);
}
}
int main() {
while (cin >> m >> n, n || m) {
memset(st, false, sizeof st);
for (int i = 0; i < n; i++) cin >> g[i];
int x, y;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (g[i][j] == '@') {
x = i, y = j;
break;
}
}
}
res = 0;
dfs(x, y);
cout << res << endl;
}
return 0;
}
二、DFS之搜索顺序
这类题目都是有一定的路程转移方式或是有一定的路线提供“一条路走到黑”的条件。
例题3、Acwing 1117. 马走日
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 30;
int T, n, m, start_x, start_y, res;
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
bool st[N][N];
void dfs(int x, int y, int step) {
if (step == n * m) { // 所走过的步数为整个棋盘的大小表示走完了整个棋盘
res++;
return;
}
for (int i = 0; i < 8; i++) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m) continue;
if (st[a][b]) continue;
st[x][y] = true;
dfs(a, b, step + 1);
st[x][y] = false;
}
}
int main() {
scanf("%d", &T);
while (T--) {
memset(st, false, sizeof st);
res = 0;
scanf("%d%d%d%d", &n, &m, &start_x, &start_y);
st[start_x][start_y] = true;
dfs(start_x, start_y, 1);
printf("%d\n", res);
}
return 0;
}
本题的难点在于想到需要先预处理出所有字符串两两之间的相连部分长度,依次作为DFS搜索中的状态转移条件。
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 25;
int n, res;
string dargon[N];
char start;
int link[N][N], used[N]; // used[i] 表示第i个字符串被用过的次数 link[i][j] 表示dargon[i], dargon[j]之间相同的字符串长度
// 输出接龙字符串的最大长度
void dfs(string strs, int last) { // 传入的是当前的接龙字符串及其编号
res = max((int)strs.size(), res);
used[last]++; // 本次用
for (int i = 0; i < n; i++) {
if (link[last][i] && used[i] < 2) {
dfs(strs + dargon[i].substr(link[last][i]), i);
}
}
used[last]--; // 回溯
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) cin >> dargon[i];
cin >> start;
// 预处理
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 查找两个字符串能否连起来
for (int k = 1; k < min(dargon[i].size(), dargon[j].size()); k++) { // 相同部分长度
if (dargon[i].substr(dargon[i].size() - k, k) == dargon[j].substr(0, k)) {
link[i][j] = k;
break;
}
}
}
}
res = 0;
for (int i = 0; i < n; i++) {
if (dargon[i][0] == start) {
dfs(dargon[i], i);
}
}
printf("%d\n", res);
return 0;
}
本题的递归转移条件显而易见就是同一组互质组中不存在两个数字最大公因数大于1。
代码 + 注释:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 15;
int n, a[N];
int res = 0x3f3f3f3f; // 最小组数
bool st[N]; // st[i] == true 表示 a[i] 已经在某个互质组中了
int g[N][N]; // g[i][j] 表示第i个组中的第j个元素,存储的是 a[] 的下标
// 求两个数字的最大公因子
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
// 判断该互质组中是否存在元素与当前数字互质
bool check(int t, int g[], int size) {
for (int i = 0; i < size; i++) {
// 当两个数的最大公因子大于1的时候,则说明两者不互质
if (gcd(a[g[i]], a[t]) > 1) return false;
}
return true;
}
// u: 当前组数, us: 该组中的数字个数, s: 已经遍历过的数字个数, start: 从那个数字开始遍历
void dfs(int u, int us, int s, int start) {
if (u >= res) return; // 剪枝
if (s == n) res = u; // 大于 res 的已经被剪枝了,剩下的一定是更优选
bool f = false; // 记录是否能够存入当前组中
for (int i = start; i < n; i++) {
if (!st[i] && check(i, g[u], us)) {
st[i] = true;
g[u][us] = i;
dfs(u, us + 1, s + 1, i + 1);
st[i] = false;
g[u][us] = 0;
f = true; // 表示能够存储
}
}
// 当无法存储的时候,就要新开一个互质组
if (!f) dfs(u + 1, 0, s, 0); // 此时当前组数加1,组中无数字,从头开始遍历查找
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
dfs(1, 0, 0, 0);
cout << res << endl;
return 0;
}
三、DFS之剪枝与优化
上面的DFS都能成功运行有一部分原因是其数据范围都比较小,当遇到数据较大的情况,需要了解一定的优化方法,也就是常说的剪枝。剪枝的分类有许多,常用的也就下面这几种:
1.优化搜索顺序
2.排除等效冗余
3.可行性剪枝
4.最优性剪枝
5.记忆化搜索(归为DP,本章不讨论)
搜索顺序为枚举每一只猫,判断其应该放在那一辆车中,具体优化剪枝在注释中有体现。
代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20;
int n, m;
int a[N];
int car[N]; // car[i] 表示第i辆车上的总重量
int res = N; // 最坏情况每只猫一辆车
void dfs(int u, int g) { // 传入参数为猫的下标和车的数量
if (g >= res) return; // 最优性剪枝
if (u == n) {
res = g;
return;
}
// 能放下的情况
for (int i = 0; i < g; i++) {
if (car[i] + a[u] <= m) {
car[i] += a[u];
dfs(u + 1, g);
car[i] -= a[u]; // 回溯
}
}
// 放不下的情况
car[g] += a[u];
dfs(u + 1, g + 1);
car[g] -= a[u]; // 回溯
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n, greater<int>()); // 优化搜索顺序
dfs(0, 1);
cout << res << endl;
return 0;
}
例题2、Acwing 166. 数独
(第一次没写出来,第二次听完才独立完成的 -_- ......)
按照一般的搜索方式在本题的数据下会超时的,因此可以使用位运算的方式表示1 ~ 9那些数字可以在当前位置上使用,用1表示;若行、列、九宫格中已经有了某数字,则用0表示无法使用,这样一个二进制状态表示的数字使用情况就是展示出来了。
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 9;
char str[N * N];
int row[N], col[N], cell[3][3]; // 行、列、九宫格
int ones[1 << N], map[1 << N]; // 二进制中1的个数、log2映射
// 初始化行、列、九宫格
void init() {
for (int i = 0; i < N; i++) row[i] = col[i] = (1 << N) - 1;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
cell[i][j] = (1 << N) - 1;
}
}
}
// 对某一位置上的元素进行操作
void draw(int x, int y, int t, bool is_set) {
if (is_set) str[x * N + y] = '1' + t;
else str[x * N + y] = '.';
int v = 1 << t;
if (!is_set) v = -v;
row[x] -= v;
col[y] -= v;
cell[x / 3][y / 3] -= v;
}
int get(int x, int y) {
return row[x] & col[y] & cell[x / 3][y / 3];
}
// 返回二进制的最后一位1
int lowbit(int x) {
return x & -x;
}
bool dfs(int cnt) {
if (!cnt) return true;
// 先找分支最少的空位
int minv = 10;
int x, y;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (str[i * N + j] == '.') {
int state = get(i, j); // 当前的状态
if (ones[state] < minv) {
minv = ones[state];
x = i, y = j;
}
}
}
}
// 从最小分支的点开始搜索
int state = get(x, y);
for (int i = state; i; i -= lowbit(i)) {
int t = map[lowbit(i)];
draw(x, y, t, true);
if (dfs(cnt - 1)) return true;
draw(x, y, t, false);
}
return false;
}
int main() {
// 预处理
for (int i = 0; i < 1 << N; i++) {
for (int j = 0; j < N; j++) {
ones[i] += i >> j & 1;
}
}
for (int i = 0; i < N; i++) map[1 << i] = i;
while (cin >> str, str[0] != 'e') {
init();
int cnt = 0; // 空位置
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (str[i * N + j] != '.') {
int t = str[i * N + j] - '1';
draw(i, j, t, true);
}
else cnt++;
}
}
dfs(cnt);
puts(str);
}
return 0;
}
例题3、Acwing 167. 木棒
和小猫爬山的思维方式很相似,都是在一个容器中装入若干元素的模型。
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 70;
int n, a[N];
int sum; // 所有木棍的总和
int len; // 每根木棍的期望长度
bool st[N]; // 判断木棍是否被用过
bool dfs(int u, int s, int start) { // 表示当前从哪根木棒开始枚举、当前木棒长度、下一次从哪里开始搜索
if (u * len == sum) return true; // 搜索结束,找到最终答案
if (s == len) return dfs(u + 1, 0, 0); // 当前木棒已经放好,搜索下一根木棒
for (int i = start; i < n; i++) {
if (st[i] || a[i] + s > len) continue;
st[i] = true;
if (dfs(u, s + a[i], i + 1)) return true;
st[i] = false; // 回溯:恢复现场
// 排除等效冗余
if (!s) return false; // 第一根木棍就失败
if (s + a[i] == len) return false; // 最后一根木棒失败
int j = i;
while (j < n && a[j] == a[i]) j++;
i = j - 1;
}
return false;
}
int main() {
while (cin >> n, n) {
memset(st, false, sizeof st);
sum = 0; // 注意:一定要初始化
for (int i = 0; i < n; i++) cin >> a[i], sum += a[i];
// 优化搜索顺序
sort(a, a + n, greater<int>());
// for (int i = 0; i < n; i++) cout << a[i] << ' ';
// puts("");
len = 1;
while (true) {
if (!(sum % len) && dfs(0, 0, 0)) {
cout << len << endl;
break;
}
else len++;
}
}
return 0;
}