搜索
搜索算法是一种“优雅”的暴力算法,它的核心思想是枚举,按照一定的顺序,不重不漏地枚举每一种可能的答案,最终找到一个问题需要的解。搜索算法是一种比较通用的算法,几乎可以实现各类问题(但是不保证高效)。
前置知识:递归、栈、队列
主要有两种搜索方法:
- 深度优先搜索(DFS)
- 宽度优先搜索(BFS)
两者主要是搜索顺序不同
深度优先搜索(DFS)
深度优先搜索是搜索算法的一种实现方式,它的思想是先尽量尝试比较“深”的答案。
在迷宫游戏中有一个非常简洁的“右手策略”:从入口处开始,用右手摸着右边的墙。一直走,一定能走出去。为什么这个策略可行呢?
迷宫里有很多岔路口,对于每个路口,把它们看成一个点,岔路口和岔路口之间,有路相连,如果把点与点之间的相邻关系抽象出来,可以得到这样的结构:
从起点出发向下走,假设有两条路可以选,分别指向岔路 1 和岔路 2,假如根据右手策略,先走到了岔路 1,此时继续根据右手策略走,结果走到了死胡同。但是,因为右手一直贴着墙,最终能从这个死胡同绕出来,回到岔路 1,此时右手摸着墙绘尝试继续走下一个方向,于是走到了终点。
通过这个例子可以发现:(1)遇到死胡同会自动走回去,回到上一个路口;(2)对于每个路口,优先尝试某个方向,继续往深处走。如果走回来了,继续尝试第二个方向、第三个方向……直到枚举完所有选择,如果还是没有找到出口,又回回到上一个路口,尝试上一个路口的下一个方向……。这样看来,右手策略确实是有效的,它的思想就是深度优先搜索。
深度优先搜索是先把一个需要解决的问题看成一个多步决策问题。要解决一个大问题,先把这个问题分成几步,而每一步都有若干种不同的决策方式,把所有决策方式都枚举一遍,并且按照某个顺序依次枚举。
对于步骤 1,有 3 种不同的决策方式,先选择决策 1,走到步骤 2 的 2 号位置。这时候又有 3 种不同的决策方式,还是先尝试第 1 种,走到 5 号位置,发现不是答案,也没有下一个步骤了,于是退回 2 号位置,尝试第 2 种决策,走到 6 号位置,发现还不是答案,于是退回 2 号位置,继续尝试走到 7 号位置,发现也不是答案,退回 2 号位置。此时 2 号位置所有可能都尝试了一遍,继续后退回 1 号位置,尝试 1 号位置的下一种可能性,走到 3 号位置,继续尝试第 1 种决策,走到 8 号位置,再退回 3 号位置,再走到 9 号位置,这时发现找到了答案。如果只需要找到一个答案,那么现在就可以让程序结束了,如果需要找到所有的答案,再退回 3 号位置继续运行程序。
深度优先搜索的名字体现在,当走到某个位置时,总是先选择一种决策方式,然后继续往深处走,尝试下一步的决策,直到走到最深处走不下去或者没有下一种决策方式时再退回。
深度优先搜索通常利用递归实现,其本质是栈。
例题:P1706 全排列问题
输入一个数字
,输出 到 的全排列。
分析:有
写一个递归函数,用一个数组记录每一层的决策结果,也就是排了哪些数,用
由于每个数字只能使用一次,在前面的决策中用过的数字,后面就不能再用了。一个简单的解决方法是在全局变量区域设置一个 used
标记数组,如果某个数字 i
在某一层用了,就把 used[i]
赋值为 true
。这样在每一层枚举决策时,都先检查一下 used
数组,如果发现这个数字没用过,才能考虑这一层可以选择放这个数,并且如果决定使用就把它标记一下。
当递归回到上一层的时候,应该清除掉当时做的对应标记,因为回来的时候就意味着接下来该层要换下一种决策方式了,那么之前做的标记就失去了意义。每次递归进入更深一层前,进行标记;从深处返回后,清除相应标记。这被称为回溯。
#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 迷宫
- 存储迷宫
二维数组 b[x][y] 存储迷宫信息,兼职判重
dx[] dy[] 数组存方向偏移量 - 深搜与回溯
从起点开始,往四个方向尝试,能走就打上标记,锁定现场,然后走过去;到达目的地则更新答案;走投无路则返回,返回后要去除标记,恢复现场;继续尝试,直到尝遍所有可能,最终从入口退出。 - 深搜实际上对应一棵DFS树
#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 跳马问题
- 用 dx[] dy[] 数组存方向偏移量
- DFS搜索方案
从起点开始,向右边的四个点尝试,能走就走过去,一直到走投无路返回;到达目的地则更新答案;一直尝试,直到穷尽所有可能
由于本题限制了跳转方向,每个点不会往回走,所以不需要判重 - 深搜会生成DFS树
#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] 八皇后
- 存储数据
ans数组记录各行放的位置
c数组标记某一列是否放置了棋子
d1数组标记副对角线,用到的下标范围是 行+列 2,3,4,...,2n
d2数组标记主对角线,用到的下标范围是 行-列+n(因为下标不能是负数,统一加n做偏移) 1,2,3,...,2n-1 - DFS搜索方案
2.1 从第1行开始放,然后尝试放第2~n行
2.2 对于某一行,依次枚举每一列,如果某一列能放下,则记住放置位置,宣布占领该位置的辐射区域,然后继续搜索下一行
2.3 如果某一行的每一列都不能放下,则退回前一行,恢复现场,尝试前一行的下一列
2.4 如果能放满n行,说明找到了一种合法方案,则方案数+1,打印方案,接着返回上一行,继续搜索其他的合法方案,直到搜完所有的可能方案
2.5 因为是逐行逐列搜的,先搜到的方案字典序一定更小
#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点
题意:输入四个整数,在每个数都只能用一次的情况下,利用加减乘除四则运算,使得最后的结果为
参考代码
#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)
- 宽搜的过程
从起点开始,向下逐层扩展,逐层访问 - 宽搜的实现
宽搜是通过队列实现的,用 queue 创建一个队列,在搜索过程中通过队列来维护序列的状态空间,入队就排队等待,出队就扩展后续状态入队
例:P1588 [USACO07OPEN] Catch That Cow S
题意:给两个数
数据范围:
5-1=4; 4*2=8; 8*2=16; 16+1=17
- 暴力搜索当前位置的三种移动方式
- 要求最小步数,应该用BFS
- 从起始位置x开始搜索,ans数组存储移动步数,当x==y时,ans[y]即答案
边界约束
x*2,x-1,x-1 不如 x-1,x*2 更优,所以上界:
减到 0 或负以后再加也不会更优,所以下界:
参考代码
#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
题意:给定一个正整数
解题思路
仍然采用 BFS 的方法
若
所以我们可以考虑根据模
参考代码
#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
- 存储网格图
f[x][y] 存储网格图
dx[8] dy[8] 存储方向偏移量 - 搜索
枚举单元格,判断是否可以进入
如果可以进入,则水坑数量+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; }
- 时间复杂度:
双向搜索
例:P1379 八数码难题
解题思路
既然是要求最少的步数,那么就可以考虑使用 BFS
这里的状态显然是当前八数码的局面,如何记录这种状态是否出现过呢?
最直接的想法就是不转化,开个
所以我们应该使用哈希表记录状态
参考代码
#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; }
康托展开
整个棋盘的任意一种状态可以看作是
康托展开是一个排列到一个自然数的双射(一一对应)
因为
是 是 是
正展开:对于第
逆展开:对于第
参考代码(康托展开)
#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; }
剪枝
剪枝就是排除搜索树中不必要的分支
比如,如果知道往某个支路走答案必定(或者已经)不如当前最优解,那么就可以跳过这个支路
同样,为了可以剪掉更多枝,可以优先往期望较优的分支走
可以对当前状态“估价”,例如当前状态到最终状态至少要
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!