深度优先搜索直通车
前言
深度优先搜索(Depth Fir Search, DFS)是用来在搜索图内搜索的一种算法。在算法中,如果遇到新状态,那么立刻处理新状态,并处理新状态转移出来的状态。若没有新状态了,那么才回去处理旧状态。DFS 一般使用递归实现。
搜索的题目灵活多变,需要有一定的经验。
例题
记录路径
按照字典序输出自然数 \(1\) 到 \(n\) 所有不重复的排列,即 \(n\) 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
我们定义一个函数 dfs(k) 表示当前搜索第 \(k\) 个数字,然后对所有的可能的状态进行遍历。我们使用 \(ans_i\) 表示第 \(i\) 个数字的值。当已经选完了 \(k\) 个数字之后,我们就打印答案。
void dfs(int k) {
if (k > n) {
for (int i = 1; i <= n; i++) {
cout << a[i] << ' ';
}
cout << '\n';
return void();
}
for (int i = 1; i <= n; i++) {
if (!f[i]) {
f[i] = 1, a[i] = i;
dfs(k + 1);
f[i] = 0;
}
}
}
可行性剪枝
将整数 \(n\) 分成 \(k\) 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。
例如:\(n=7\),\(k=3\),下面三种分法被认为是相同的。
\(1,1,5\);
\(1,5,1\);
\(5,1,1\).
问有多少种不同的分法。
根据题目,我们可以写出 dfs(int x, int s, int l)。表示当前是第 \(x\) 个数,和为 \(s\),上一个数是 \(l\)。因为不考虑顺序的话,每个数必须大于等于前面的数,因此需要一个变量 \(l\) 来记录。
void dfs(int x, int s, int l) {
if (x == k) { // 如果已经选完了
ans += s == n; // 判断是否正好等于 n
return;
}
for (int i = l; i <= n; i++) { // 从 l 开始枚举
dfs(i + 1, s + j, j);
}
}
但是我们 TLE 了……因为有可能我们累积的和已经大于 \(n\) 了,但是仍然还在进行搜索。很简单,我们在 dfs 里面加一个判断就可以了!
void dfs(int x, int s, int l) {
if (s > n) {
return;
}
if (x == k) { // 如果已经选完了
ans += s == n; // 判断是否正好等于 n
return;
}
for (int i = l; i <= n; i++) { // 从 l 开始枚举
dfs(i + 1, s + j, j);
}
}
但是还是 TLE 了一个点……我们知道,递归的速度是很慢的,不如我们直接减少递归,在递归前就剪枝。很简单,我们只枚举到 \(n-s\) 就行了,这样子递归后一定满足要求。
#include <iostream>
using namespace std;
int n, k, ans;
void dfs(int i, int s, int l) {
if (i == k) {
ans += s == n;
return;
}
for (int j = l; j <= n - s; ++j) {
dfs(i + 1, s + j, j);
}
}
int main() {
cin >> n >> k;
dfs(0, 0, 1);
cout << ans << '\n';
return 0;
}
顺利 AC!这就是可行性剪枝。当当前状态已经无法产生答案的时候,我们就不再进行搜索了。
状态判重
年轻的拉尔夫开玩笑地从一个小镇上偷走了一辆车,但他没想到的是那辆车属于警察局,并且车上装有用于发射车子移动路线的装置。那个装置太旧了,以至于只能发射关于那辆车的移动路线的方向信息。
编写程序,通过使用一张小镇的地图帮助警察局找到那辆车。程序必须能表示出该车最终所有可能的位置。
小镇的地图是矩形的,上面的符号用来标明哪儿可以行车哪儿不行。. 表示小镇上那块地方是可以行车的,而符号 X 表示此处不能行车。拉尔夫所开小车的初始位置用字符的 * 表示,且汽车能从初始位置通过。汽车能向四个方向移动:向北(向上),向南(向下),向西(向左),向东(向右)。
拉尔夫所开小车的行动路线是通过一组给定的方向来描述的。在每个给定的方向,拉尔夫驾驶小车通过小镇上一个或更多的可行车地点。
输入格式
输入文件的第一行包含两个用空格隔开的自然数 \(R\) 和 \(C\),\(1\le R\le 50\),\(1\le C\le 50\),分别表示小镇地图中的行数和列数。
以下的 \(R\) 行中每行都包含一组 \(C\) 个符号(. 或 X 或 *)用来描述地图上相应的部位。接下来的第 \(R+2\) 行包含一个自然数 \(N\),\(1\le N\le 1000\),表示一组方向的长度。
接下来的 \(N\) 行幅行包含下述单词中的任一个:NORTH(北)、SOUTH(南)、WEST(西)和 EAST(东),表示汽车移动的方向,任何两个连续的方向都不相同。
输出格式
用 \(R\) 行表示的小镇的地图(像输入文件中一样),字符 \(\verb|*|\) 应该仅用来表示汽车最终可能出现的位置。
解法
这题就是一个暴力的 dfs。定义函数 \verb|dfs(x, y, i, b)| 表示当前坐标为 \((x,y)\),当前的操作是 \(i\),\(b\) 表示是否可以转向。然后进行 dfs 就行。但是当我们到了同一个坐标,操作编号也完全相同时,此时再次搜索一遍没有任何意义,因此我们直接 return。这就叫做状态判重。
#include <cstring>
#include <iostream>
using namespace std;
const int kMaxR = 51, kMaxN = 1001;
int o[kMaxN], dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}, r, c, n, x, y;
char a[kMaxR][kMaxR]; // 地图
bool v[kMaxR][kMaxR][kMaxN]; // 判重数组
string s;
void dfs(int x, int y, int i, bool b) {
if (i > n || x < 1 || x > r || y < 1 || y > c ||
a[x][y] == 'X' || v[x][y][i]) {
return;
}
// 如果已经走完了操作、超出了地图边界、已经走过了、当前地点为障碍那么就 return
if (i == n) { // 如果正好走完操作
a[x][y] = '*'; // 标记为可能的重点
}
v[x][y][i] = 1; // 标记已经走过了
dfs(x + dx[o[i]], y + dy[o[i]], i, 1); // 不转向
if (b) { // 如果可以转向
dfs(x + dx[o[i + 1]], y + dy[o[i + 1]], i + 1, 1); // 转向
}
}
int main() {
cin >> r >> c;
for (int i = 1; i <= r; ++i) {
for (int j = 1; j <= c; ++j) {
cin >> a[i][j];
if (a[i][j] == '*') {
x = i, y = j, a[i][j] = '.';
}
}
}
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> s;
if (s == "NORTH") {
o[i] = 0;
} else if (s == "SOUTH") {
o[i] = 1;
} else if (s == "WEST") {
o[i] = 2;
} else {
o[i] = 3;
}
}
dfs(x, y, 1, 0);
for (int i = 1; i <= r; ++i) {
for (int j = 1; j <= c; ++j) {
cout << a[i][j];
}
cout << '\n';
}
return 0;
}
最优性剪枝
本题使用 Special Judge。
Farmer John 把农场划分为了一个 \(r\) 行 \(c\) 列的矩阵,并发现奶牛们无法通过其中一些区域。此刻,Bessie 位于坐标为 \((1,1)\) 的区域,并想到坐标为 \((r,c)\) 的牛棚享用晚餐。她知道,以她所在的区域为起点,每次移动至相邻的四个区域之一,总有一些路径可以到达牛棚。
这样的路径可能有无数种,请你输出任意一种,并保证所需移动次数不超过 \(100000\)。
输入格式
第一行两个整数 \(r,c\)。接下来 \(r\) 行,每行 \(c\) 个字符,表示 Bessie 能否通过相应位置的区域。字符只可能是 . 或 *。
.表示 Bessie 可以通过该区域。*表示 Bessie 无法通过该区域。
输出格式
若干行,每行包含两个用空格隔开的整数,表示 Bessie 依次通过的区域的坐标。显然,输出的第一行是 \verb|1 1|,最后一行是 \verb|r c|。相邻的两个坐标所表示的区域必须相邻。
解法
这题其实十分的暴力。使用 vector 记录路径,并写下状态判重的数组,当遇到重复状态的时候就剪枝。但是这样子是非常错误的,因为有可能后面到了那个点会比前面的步数更少,因此我们就定义一个数组 \(f_{(i,j)}\) 表示 \((1,1)\) 到 \((r,c)\) 的最小步数。当当前的状态已经劣于以前的状态时,我们就剪枝。这就叫做最优性剪枝。
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
const int kMaxN = 250, kD[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int f[kMaxN][kMaxN], r, c, ans; // 最优化数组
char a[kMaxN][kMaxN];
vector<pair<int, int>> v, vec; // 记录路径
void dfs(int x, int y, int s) {
if (x < 1 || x > r || y < 1 || y > c ||
a[x][y] == '*' || s >= f[x][y] || v.size() > 1e5) {
return;
}
// 如果超出了地图范围、当前点位不可走、答案过大就剪枝
if (x == r && y == c) { // 如果走到了地方
if (s < ans) { // 如果比答案更优
vec = v, ans = s; // 赋值
}
return;
}
f[x][y] = s; // 记录
for (auto i : kD) { // 枚举走向
int nx = x + i[0], ny = y + i[1];
v.push_back({nx, ny}), dfs(nx, ny, s + 1), v.pop_back();
}
}
int main() {
fill(f[0], f[0] + kMaxN * kMaxN, 1e9);
cin >> r >> c, ans = 1e9;
for (int i = 1; i <= r; ++i) {
for (int j = 1; j <= c; ++j) {
cin >> a[i][j];
}
}
v.push_back({1, 1}), dfs(1, 1, 0);
for (auto i : vec) {
cout << i.first << ' ' << i.second << '\n';
}
return 0;
}

浙公网安备 33010602011771号