搜索

搜索算法是一种“优雅”的暴力算法,它的核心思想是枚举,按照一定的顺序,不重不漏地枚举每一种可能的答案,最终找到一个问题需要的解。搜索算法是一种比较通用的算法,几乎可以实现各类问题(但是不保证高效)。

前置知识:递归、栈、队列

主要有两种搜索方法:

  1. 深度优先搜索(DFS)
  2. 宽度优先搜索(BFS)

两者主要是搜索顺序不同

深度优先搜索(DFS)

深度优先搜索是搜索算法的一种实现方式,它的思想是先尽量尝试比较“深”的答案。

image

在迷宫游戏中有一个非常简洁的“右手策略”:从入口处开始,用右手摸着右边的墙。一直走,一定能走出去。为什么这个策略可行呢?

迷宫里有很多岔路口,对于每个路口,把它们看成一个点,岔路口和岔路口之间,有路相连,如果把点与点之间的相邻关系抽象出来,可以得到这样的结构:

image

从起点出发向下走,假设有两条路可以选,分别指向岔路 1 和岔路 2,假如根据右手策略,先走到了岔路 1,此时继续根据右手策略走,结果走到了死胡同。但是,因为右手一直贴着墙,最终能从这个死胡同绕出来,回到岔路 1,此时右手摸着墙绘尝试继续走下一个方向,于是走到了终点。

通过这个例子可以发现:(1)遇到死胡同会自动走回去,回到上一个路口;(2)对于每个路口,优先尝试某个方向,继续往深处走。如果走回来了,继续尝试第二个方向、第三个方向……直到枚举完所有选择,如果还是没有找到出口,又回回到上一个路口,尝试上一个路口的下一个方向……。这样看来,右手策略确实是有效的,它的思想就是深度优先搜索。

深度优先搜索是先把一个需要解决的问题看成一个多步决策问题。要解决一个大问题,先把这个问题分成几步,而每一步都有若干种不同的决策方式,把所有决策方式都枚举一遍,并且按照某个顺序依次枚举。

image

对于步骤 1,有 3 种不同的决策方式,先选择决策 1,走到步骤 2 的 2 号位置。这时候又有 3 种不同的决策方式,还是先尝试第 1 种,走到 5 号位置,发现不是答案,也没有下一个步骤了,于是退回 2 号位置,尝试第 2 种决策,走到 6 号位置,发现还不是答案,于是退回 2 号位置,继续尝试走到 7 号位置,发现也不是答案,退回 2 号位置。此时 2 号位置所有可能都尝试了一遍,继续后退回 1 号位置,尝试 1 号位置的下一种可能性,走到 3 号位置,继续尝试第 1 种决策,走到 8 号位置,再退回 3 号位置,再走到 9 号位置,这时发现找到了答案。如果只需要找到一个答案,那么现在就可以让程序结束了,如果需要找到所有的答案,再退回 3 号位置继续运行程序。

深度优先搜索的名字体现在,当走到某个位置时,总是先选择一种决策方式,然后继续往深处走,尝试下一步的决策,直到走到最深处走不下去或者没有下一种决策方式时再退回。

深度优先搜索通常利用递归实现,其本质是栈。

例题:P1706 全排列问题

输入一个数字 n,输出 1n 的全排列。

分析:有 n 个数字需要全排列,可以看成一个多步决策问题:有 n 个位置需要放数字,每个位置放一个,对每个位置放什么数字进行决策。

写一个递归函数,用一个数组记录每一层的决策结果,也就是排了哪些数,用 k 表示目前走到第几层,即现在正在枚举第 k 个位置要放的数字。当走到第 n+1 层时就可以数出结果了。

由于每个数字只能使用一次,在前面的决策中用过的数字,后面就不能再用了。一个简单的解决方法是在全局变量区域设置一个 used 标记数组,如果某个数字 i 在某一层用了,就把 used[i] 赋值为 true。这样在每一层枚举决策时,都先检查一下 used 数组,如果发现这个数字没用过,才能考虑这一层可以选择放这个数,并且如果决定使用就把它标记一下。

当递归回到上一层的时候,应该清除掉当时做的对应标记,因为回来的时候就意味着接下来该层要换下一种决策方式了,那么之前做的标记就失去了意义。每次递归进入更深一层前,进行标记;从深处返回后,清除相应标记。这被称为回溯。

image

#include <cstdio>
const int N = 10;
int a[N], n; // a数组记录每一个位置放的数
bool used[N]; // 记录每个数字是否用过的标记数组
void dfs(int k) { // k表示当前正在放第几个数
if (k == n + 1) { // 如果走到了第n+1层,说明已经完成了一种全排列
for (int i = 1; i <= n; i++) {
// 输出时%5d表示保留5个场宽(右对齐占据5个宽度)
// 如果长度不够5位,会在前面自动补空格
printf("%5d", a[i]);
}
printf("\n");
return;
}
for (int i = 1; i <= n; i++) { // 尝试在第k个位置放i
if (!used[i]) { // 如果i没用过
a[k] = i; used[i] = true; // 在当前位置放i,并将其标记已使用
dfs(k + 1); // 去下一层
used[i] = false; // 回溯,清除标记
}
}
}
int main()
{
scanf("%d", &n);
dfs(1);
return 0;
}

例:P1605 迷宫

  1. 存储迷宫
    二维数组 b[x][y] 存储迷宫信息,兼职判重
    dx[] dy[] 数组存方向偏移量
  2. 深搜与回溯
    从起点开始,往四个方向尝试,能走就打上标记,锁定现场,然后走过去;到达目的地则更新答案;走投无路则返回,返回后要去除标记,恢复现场;继续尝试,直到尝遍所有可能,最终从入口退出。
  3. 深搜实际上对应一棵DFS树
    image
#include <cstdio>
int n, m, t, ans;
int sx, sy, fx, fy;
int b[10][10];
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
void dfs(int x, int y) {
if (x == fx && y == fy) { // 递归的边界条件
++ans;
return;
}
// 还没到终点
for (int i = 0; i < 4; ++i) {
int xx = x + dx[i];
int yy = y + dy[i];
if (xx >= 1 && xx <= n && yy >= 1 && yy <= m && b[xx][yy] == 0) {
b[xx][yy] = 1; // (xx,yy)被走过
dfs(xx, yy); // (x,y)->(xx,yy)
b[xx][yy] = 0; // 复原
}
}
}
int main()
{
scanf("%d%d%d%d%d%d%d", &n, &m, &t, &sx, &sy, &fx, &fy);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
b[i][j] = 0; // 能走
for (int i = 0; i < t; ++i) {
int x, y;
scanf("%d%d", &x, &y);
b[x][y] = 2; // 障碍物
}
b[sx][sy] = 1; // 暂时不能走(被当前路径走过)
dfs(sx, sy);
printf("%d\n", ans);
return 0;
}

例:P1644 跳马问题

  1. 用 dx[] dy[] 数组存方向偏移量
  2. DFS搜索方案
    从起点开始,向右边的四个点尝试,能走就走过去,一直到走投无路返回;到达目的地则更新答案;一直尝试,直到穷尽所有可能
    由于本题限制了跳转方向,每个点不会往回走,所以不需要判重
  3. 深搜会生成DFS树
    image
#include <cstdio>
const int MAXN = 20;
int n, m, ans;
int dx[4] = {2, 1, -1, -2};
int dy[4] = {1, 2, 2, 1};
void dfs(int x, int y) {
if (x == n && y == m) {
ans++;
return;
}
for (int i = 0; i < 4; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (xx >= 0 && xx <= n && yy <= m) dfs(xx, yy);
}
}
int main()
{
scanf("%d%d", &n, &m);
dfs(0, 0);
printf("%d\n", ans);
return 0;
}

例:P1219 [USACO1.5] 八皇后

  1. 存储数据
    ans数组记录各行放的位置
    c数组标记某一列是否放置了棋子
    d1数组标记副对角线,用到的下标范围是 行+列 2,3,4,...,2n
    d2数组标记主对角线,用到的下标范围是 行-列+n(因为下标不能是负数,统一加n做偏移) 1,2,3,...,2n-1
  2. DFS搜索方案
    2.1 从第1行开始放,然后尝试放第2~n行
    2.2 对于某一行,依次枚举每一列,如果某一列能放下,则记住放置位置,宣布占领该位置的辐射区域,然后继续搜索下一行
    2.3 如果某一行的每一列都不能放下,则退回前一行,恢复现场,尝试前一行的下一列
    2.4 如果能放满n行,说明找到了一种合法方案,则方案数+1,打印方案,接着返回上一行,继续搜索其他的合法方案,直到搜完所有的可能方案
    2.5 因为是逐行逐列搜的,先搜到的方案字典序一定更小
    image
#include <cstdio>
const int MAXN = 30;
int n, cnt;
int ans[MAXN], c[MAXN], d1[MAXN], d2[MAXN];
void dfs(int i) {
if (i > n) {
cnt++;
if (cnt <= 3) {
for (int j = 1; j <= n; j++) printf("%d ", ans[j]);
printf("\n");
}
return;
}
for (int j = 1; j <= n; j++) {
if (!c[j] && !d1[i + j] && !d2[i - j + n]) {
ans[i] = j; // 记录第i行的皇后放在了第j列
c[j] = d1[i + j] = d2[i - j + n] = 1; // 标记
dfs(i + 1);
c[j] = d1[i + j] = d2[i - j + n] = 0; // 复原
}
}
}
int main()
{
scanf("%d", &n);
dfs(1);
printf("%d\n", cnt);
return 0;
}

例:P1236 算24点

题意:输入四个整数,在每个数都只能用一次的情况下,利用加减乘除四则运算,使得最后的结果为 24,给出一组解或输出无解

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
int num[10], res_idx;
bool used[10], flag;
char buf[4][100];
void dfs(int idx, int pre) {
if (idx == 4) {
if (pre == 24) {
for (int i = 1; i <= 3; i++) printf("%s\n", buf[i]);
flag = true;
}
return;
}
for (int i = 1; i < res_idx; i++)
for (int j = 1; j < res_idx; j++) {
if (i == j || used[i] || used[j]) continue;
// 选出num[i]和num[j]进行运算
used[i] = used[j] = true;
int big = max(num[i], num[j]), small = min(num[i], num[j]);
sprintf(buf[idx], "%d+%d=%d", big, small, big + small);
num[res_idx] = big + small; res_idx++;
dfs(idx + 1, big + small);
if (flag) return;
res_idx--;
if (big >= small) {
num[res_idx] = big - small; res_idx++;
sprintf(buf[idx], "%d-%d=%d", big, small, big - small);
dfs(idx + 1, big - small);
if (flag) return;
res_idx--;
}
sprintf(buf[idx], "%d*%d=%d", big, small, big * small);
num[res_idx] = big * small; res_idx++;
dfs(idx + 1, big * small);
if (flag) return;
res_idx--;
if (small != 0 && big % small == 0) {
num[res_idx] = big / small; res_idx++;
sprintf(buf[idx], "%d/%d=%d", big, small, big / small);
dfs(idx + 1, big / small);
if (flag) return;
res_idx--;
}
used[i] = used[j] = false;
}
}
int main()
{
for (int i = 1; i <= 4; i++) scanf("%d", &num[i]);
res_idx = 5;
dfs(1, 0);
if (!flag) printf("No answer!\n");
return 0;
}

宽度优先搜索(BFS)

  1. 宽搜的过程
    从起点开始,向下逐层扩展,逐层访问
  2. 宽搜的实现
    宽搜是通过队列实现的,用 queue 创建一个队列,在搜索过程中通过队列来维护序列的状态空间,入队就排队等待,出队就扩展后续状态入队

例:P1588 [USACO07OPEN] Catch That Cow S

题意:给两个数 x,y,每次可以把 x 变成 x1,x+1,2x,问经过多少次变化可以让 xy 相等
数据范围:x,y105

5-1=4; 4*2=8; 8*2=16; 16+1=17

  1. 暴力搜索当前位置的三种移动方式
  2. 要求最小步数,应该用BFS
  3. 从起始位置x开始搜索,ans数组存储移动步数,当x==y时,ans[y]即答案

边界约束 (1x105) 与去重
x*2,x-1,x-1 不如 x-1,x*2 更优,所以上界:x105
减到 0 或负以后再加也不会更优,所以下界:x1
image

参考代码
#include <cstdio>
#include <queue>
using std::queue;
const int N = 1e5+5;
int ans[N]; // 表示从x到对应的数最少需要做几次变换
bool vis[N]; // 表示某个数有没有被搜到过
int main()
{
int T; scanf("%d",&T);
for (int i=1;i<=T;i++) {
int x, y; scanf("%d%d",&x,&y);
queue<int> q; q.push(x);
// 注意vis和ans的初始化
for (int j=0;j<N;j++) {
ans[j]=0; vis[j]=false;
}
// 以上是初始化(这对于多组数据问题非常关键)
vis[x]=true; ans[x]=0;
while (!q.empty()) {
// 取出队列的头部元素
int t = q.front();
q.pop();
if (t==y) {
// 已经搜到y了,提前结束
break;
}
// 引出新的转移 t-1,t+1,t*2
int t1 = t-1;
if (t1>=0 && t1<N && !vis[t1]) {
q.push(t1); vis[t1]=true;
ans[t1]=ans[t]+1;
}
int t2 = t+1;
if (t2>=0 && t2<N && !vis[t2]) {
q.push(t2); vis[t2]=true;
ans[t2]=ans[t]+1;
}
int t3 = t*2;
if (t3>=0 && t3<N && !vis[t3]) {
q.push(t3); vis[t3]=true;
ans[t3]=ans[t]+1;
}
}
printf("%d\n",ans[y]);
}
return 0;
}

例:P1443 马的遍历

参考代码
#include <cstdio>
#include <queue>
using std::queue;
const int N = 405;
bool vis[N][N];
int ans[N][N];
int dx[8] = {-1, -2, -2, -1, 1, 2, 2, 1};
int dy[8] = {-2, -1, 1, 2, -2, -1, 1, 2};
struct S {
int x,y;
};
int main()
{
int n, m, x, y; scanf("%d%d%d%d",&n,&m,&x,&y);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++) {
ans[i][j]=-1;
}
queue<S> q; q.push({x,y});
vis[x][y]=true; ans[x][y]=0;
while (!q.empty()) {
S t = q.front(); q.pop();
for (int i=0;i<8;i++) {
int tx=t.x+dx[i];
int ty=t.y+dy[i];
// (t.x,t.y) -> (tx,ty)
if (tx>=1 && tx<=n && ty>=1 && ty<=m && !vis[tx][ty]) {
q.push({tx,ty});
ans[tx][ty]=ans[t.x][t.y]+1;
vis[tx][ty]=true;
}
}
}
for (int i=1;i<=n;i++) {
for (int j=1;j<=m;j++) {
printf("%-5d",ans[i][j]); // %-5d 是左对齐占满5个单位
}
printf("\n");
}
return 0;
}

例:UVA1189 Find The Multiple

题意:给定一个正整数 n,寻找 n 的一个非零的倍数 m,这个 m 在十进制表示下每一位只包含 0 或者 1

解题思路

仍然采用 BFS 的方法

x 后面加上 0n 取模就是 x10modn,后面加上 1n 取模就是 (x10+1)modn
xyn 相等,那么在两数后面添加相同的数之后模 n 也是一样的
所以我们可以考虑根据模 n 的值进行 BFS,则时间复杂度为 O(n)

参考代码
#include <cstdio>
#include <queue>
using namespace std;
const int N = 205;
int pre[N], num[N];
// num[i]表示模n余i的数对应的位
// pre[i]表示该状态的前一个状态
void output(int x) {
if (x == -1) return;
output(pre[x]); // 利用递归实现倒序输出
printf("%d", num[x]);
}
int main()
{
int n;
scanf("%d", &n);
while (n != 0) {
for (int i = 0; i < n; i++) {
pre[i] = num[i] = -1;
}
queue<int> q;
q.push(1 % n); num[1 % n] = 1;
while (!q.empty()) {
int cur = q.front(); q.pop();
if (cur == 0) {
output(cur); printf("\n");
break;
}
// 在后面加一个0
int to = cur * 10 % n;
if (num[to] == -1) {
q.push(to);
num[to] = 0;
pre[to] = cur;
}
// 在后面加一个1
to = (cur * 10 + 1) % n;
if (num[to] == -1) {
q.push(to);
num[to] = 1;
pre[to] = cur;
}
}
scanf("%d", &n);
}
return 0;
}

例:UVA12101 Prime Path

题意:把一个四位数质数变成另一个四位数质数,每次只能改变一个数位,每次改变后的四位数要求是质数,求最少修改次数

要求最少修改次数,显然应该使用 BFS

参考代码
#include <cstdio>
#include <queue>
using namespace std;
const int N = 10005;
int ans[N];
bool p[N], vis[N];
int calc(int d1, int d2, int d3, int d4) {
return d1 * 1000 + d2 * 100 + d3 * 10 + d4;
}
int main()
{
// 预处理出10000以内的素数,p[i]表示i是不是素数
for (int i = 2; i < N; i++) p[i] = true;
for (int i = 2; i < N / i; i++) {
if (p[i]) {
for (int j = i * i; j < N; j += i) p[j] = false;
}
}
int t;
scanf("%d", &t);
while (t--) {
int a, b;
scanf("%d%d", &a, &b);
for (int i = 1000; i < 10000; i++) {
vis[i] = false; ans[i] = 0;
}
queue<int> q; q.push(a); vis[a] = true;
while (!q.empty()) {
int cur = q.front(); q.pop();
if (cur == b) break;
int d1 = cur / 1000;
int d2 = cur / 100 % 10;
int d3 = cur / 10 % 10;
int d4 = cur % 10;
// 改千位
for (int i = 1; i <= 9; i++) {
int num = calc(i, d2, d3, d4);
// num还没有变过且是质数才继续搜索该状态
if (!vis[num] && p[num]) {
q.push(num); vis[num] = true;
ans[num] = ans[cur] + 1;
}
}
// 改百位
for (int i = 0; i <= 9; i++) {
int num = calc(d1, i, d3, d4);
if (!vis[num] && p[num]) {
q.push(num); vis[num] = true;
ans[num] = ans[cur] + 1;
}
}
// 改十位
for (int i = 0; i <= 9; i++) {
int num = calc(d1, d2, i, d4);
if (!vis[num] && p[num]) {
q.push(num); vis[num] = true;
ans[num] = ans[cur] + 1;
}
}
// 改个位
for (int i = 0; i <= 9; i++) {
int num = calc(d1, d2, d3, i);
if (!vis[num] && p[num]) {
q.push(num); vis[num] = true;
ans[num] = ans[cur] + 1;
}
}
}
if (!vis[b]) printf("Impossible\n");
else printf("%d\n", ans[b]);
}
return 0;
}

洪水填充(flood fill)

  • 判断连通性和统计连通块个数的问题

例:P1596 [USACO10OCT] Lake Counting S

  1. 存储网格图
    f[x][y] 存储网格图
    dx[8] dy[8] 存储方向偏移量
  2. 搜索
    枚举单元格,判断是否可以进入
    如果可以进入,则水坑数量+1,并且将该单元格所属水坑的其他单元格全都进入一遍(这里DFS和BFS都可实现)
    为避免重复搜索,对走过的单元格进行标记

DFS实现

#include <cstdio>
char f[105][105];
int n, m;
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
void dfs(int x, int y) {
f[x][y] = '.';
for (int i = 0; i < 8; i++) {
int xx = x + dx[i];
int yy = y + dy[i];
if (xx >= 0 && xx < n && yy >= 0 && yy < m && f[xx][yy] == 'W') {
dfs(xx, yy);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%s", f[i]);
int lake = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (f[i][j] == 'W') {
lake++;
dfs(i, j);
}
printf("%d\n", lake);
return 0;
}

BFS实现

#include <cstdio>
#include <queue>
using namespace std;
char f[105][105];
int n, m;
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
struct Node {
int x, y;
};
void bfs(int x, int y) {
f[x][y] = '.';
queue<Node> q;
q.push({x, y});
while (!q.empty()) {
Node t = q.front(); q.pop();
for (int i = 0; i < 8; i++) {
int xx = t.x + dx[i], yy = t.y + dy[i];
if (xx >= 0 && xx < n && yy >= 0 && yy < m && f[xx][yy] == 'W') {
f[xx][yy] = '.';
q.push({xx, yy});
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%s", f[i]);
int lake = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (f[i][j] == 'W') {
lake++;
bfs(i, j);
}
printf("%d\n", lake);
return 0;
}
  • 时间复杂度:O(nm)

双向搜索

image

例:P1379 八数码难题

解题思路

既然是要求最少的步数,那么就可以考虑使用 BFS
这里的状态显然是当前八数码的局面,如何记录这种状态是否出现过呢?
最直接的想法就是不转化,开个 9 维的数组,但是这样会造成巨大的空间复杂度
所以我们应该使用哈希表记录状态

参考代码
#include <cstdio>
#include <iostream>
#include <string>
#include <queue>
#include <unordered_map>
#include <algorithm>
using namespace std;
unordered_map<string, int> ans;
struct Status {
string s;
int idx;
};
int main()
{
string s, target = "123804765";
cin >> s;
queue<Status> q;
q.push({s, int(s.find('0'))}); ans[s] = 0;
while (!q.empty()) {
Status cur = q.front();
q.pop();
if (cur.s == target) break;
int step = ans[cur.s];
// 与上面交换
if (cur.idx > 2) {
string to = cur.s;
swap(to[cur.idx - 3], to[cur.idx]);
if (ans.count(to) == 0) {
q.push({to, cur.idx - 3});
ans[to] = step + 1;
}
}
// 与左边交换
if (cur.idx % 3 > 0) {
string to = cur.s;
swap(to[cur.idx - 1], to[cur.idx]);
if (ans.count(to) == 0) {
q.push({to, cur.idx - 1});
ans[to] = step + 1;
}
}
// 与右边交换
if (cur.idx % 3 < 2) {
string to = cur.s;
swap(to[cur.idx + 1], to[cur.idx]);
if (ans.count(to) == 0) {
q.push({to, cur.idx + 1});
ans[to] = step + 1;
}
}
// 与下面交换
if (cur.idx < 6) {
string to = cur.s;
swap(to[cur.idx + 3], to[cur.idx]);
if (ans.count(to) == 0) {
q.push({to, cur.idx + 3});
ans[to] = step + 1;
}
}
}
printf("%d\n", ans[target]);
return 0;
}
康托展开

整个棋盘的任意一种状态可以看作是 08 的一组排列
康托展开是一个排列到一个自然数的双射(一一对应)
因为 n 个数的排列一共有 n! 种,所以可以将一个排列用它在所有排列中的排名来表示(可以理解为字典序),例如:

  • 0123456780
  • 8765432109!1=362879
  • 05421367808!+47!+36!+15!+04!+03!+02!+01!+00!=22440

正展开:对于第 i 个数,统计有几个排列前 i1 个数和它相同,第 i 个数比它小

逆展开:对于第 i 个数,统计当前值可以减去几个 (ni)!,就说明有几个未出现数比它小,即可求得第 i 个数的值

参考代码(康托展开)
#include <cstdio>
#include <iostream>
#include <string>
#include <queue>
#include <algorithm>
using namespace std;
const int FAC = 362880 + 5;
int ans[FAC], fac[10];
bool used[10];
struct Status {
string s;
int idx;
};
void init() {
fac[0] = 1;
for (int i = 1; i < 10; i++) fac[i] = fac[i - 1] * i;
for (int i = 0; i < FAC; i++) ans[i] = -1;
}
int cantor(const string& s) {
for (int i = 0; i < 10; i++) used[i] = false;
int ret = 0, len = s.length();
for (int i = 0; i < len; i++) {
int cnt = 0, num = s[i] - '0';
for (int j = 0; j < num; j++)
if (!used[j]) cnt++;
ret += cnt * fac[len - i - 1];
used[num] = true;
}
return ret;
}
int main()
{
init();
string s, target = "123804765";
cin >> s;
queue<Status> q;
q.push({s, int(s.find('0'))}); ans[cantor(s)] = 0;
while (!q.empty()) {
Status cur = q.front();
q.pop();
if (cur.s == target) break;
int step = ans[cantor(cur.s)];
// 与上面交换
if (cur.idx > 2) {
string to = cur.s;
swap(to[cur.idx - 3], to[cur.idx]);
int perm = cantor(to);
if (ans[perm] == -1) {
q.push({to, cur.idx - 3});
ans[perm] = step + 1;
}
}
// 与左边交换
if (cur.idx % 3 > 0) {
string to = cur.s;
swap(to[cur.idx - 1], to[cur.idx]);
int perm = cantor(to);
if (ans[perm] == -1) {
q.push({to, cur.idx - 1});
ans[perm] = step + 1;
}
}
// 与右边交换
if (cur.idx % 3 < 2) {
string to = cur.s;
swap(to[cur.idx + 1], to[cur.idx]);
int perm = cantor(to);
if (ans[perm] == -1) {
q.push({to, cur.idx + 1});
ans[perm] = step + 1;
}
}
// 与下面交换
if (cur.idx < 6) {
string to = cur.s;
swap(to[cur.idx + 3], to[cur.idx]);
int perm = cantor(to);
if (ans[perm] == -1) {
q.push({to, cur.idx + 3});
ans[perm] = step + 1;
}
}
}
printf("%d\n", ans[cantor(target)]);
return 0;
}

还可以更快吗?

双向搜索

可以发现搜索的起点和终点都是已知的,是不是可以同时从终点开始 BFS,在中间点交汇呢?

为了减少搜索量,我们可以从起点和终点同时开始进行 BFS 或者 DFS,当两边的搜索结果相遇时,就可以认为是获得了可行解

显然经此优化,搜索量仅为原来的一半

参考代码(双向搜索)
#include <cstdio>
#include <iostream>
#include <string>
#include <queue>
#include <unordered_map>
#include <algorithm>
using namespace std;
unordered_map<string, int> ans[2];
struct Status {
string s;
int idx;
};
queue<Status> q[2];
int main()
{
string s, target = "123804765";
cin >> s;
q[0].push({s, int(s.find('0'))}); ans[0][s] = 0;
q[1].push({target, int(target.find('0'))}); ans[1][target] = 0;
int now = 0;
while (true) {
int rev = 1 - now;
Status cur = q[now].front(); q[now].pop();
if (ans[rev].count(cur.s)) {
printf("%d\n", ans[now][cur.s] + ans[rev][cur.s]);
break;
}
int step = ans[now][cur.s];
// 与上面交换
if (cur.idx > 2) {
string to = cur.s;
swap(to[cur.idx - 3], to[cur.idx]);
if (ans[now].count(to) == 0) {
q[now].push({to, cur.idx - 3});
ans[now][to] = step + 1;
}
}
// 与左边交换
if (cur.idx % 3 > 0) {
string to = cur.s;
swap(to[cur.idx - 1], to[cur.idx]);
if (ans[now].count(to) == 0) {
q[now].push({to, cur.idx - 1});
ans[now][to] = step + 1;
}
}
// 与右边交换
if (cur.idx % 3 < 2) {
string to = cur.s;
swap(to[cur.idx + 1], to[cur.idx]);
if (ans[now].count(to) == 0) {
q[now].push({to, cur.idx + 1});
ans[now][to] = step + 1;
}
}
// 与下面交换
if (cur.idx < 6) {
string to = cur.s;
swap(to[cur.idx + 3], to[cur.idx]);
if (ans[now].count(to) == 0) {
q[now].push({to, cur.idx + 3});
ans[now][to] = step + 1;
}
}
now = rev;
}
return 0;
}

剪枝

剪枝就是排除搜索树中不必要的分支
比如,如果知道往某个支路走答案必定(或者已经)不如当前最优解,那么就可以跳过这个支路
同样,为了可以剪掉更多枝,可以优先往期望较优的分支走
可以对当前状态“估价”,例如当前状态到最终状态至少要 x 步,而当前已经走过的步数再加上 x 大于等于当前的最优解步数,则直接回溯

posted @   RonChen  阅读(222)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示