深度优先搜索直通车

前言

深度优先搜索(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;
}
posted @ 2023-10-15 16:22  haokee  阅读(24)  评论(0)    收藏  举报