搜索
搜索题的典型特征:通常数据范围较小,时间复杂度为指数级
DFS
会在一条路上走到底再回溯,采用递归形式
剪枝
1. 优化搜索顺序
小木棍
将木棒长度排序,优先考虑长的木棒
数独问题 (数独、数独2、靶形数独)
枚举未填格子时,优先考虑能填数字最少的格子
虫食算
从竖式的最右端开始搜索,这样可以使三个数字均已填出的竖列更多,提升剪枝效率
2. 排除等效冗余
小木棍
- 限制同一根木棒中木棍长度递减,不走回头路,排列 -> 组合
- 存在相等数据:记录失败数据,避免重复尝试
- 如果当前木棒尝试拼入第一根木棍A就失败,则立即回溯。因为几根没有拼的木棒是等价的,无论木棍A拼入哪一根都会失败,不然我们只需调整该成功方案中木棒顺序,就可得到使本次搜索成功的方案。
- 如果一根木棒刚好拼完,但后续搜索失败,立即回溯。因为用若干根小木棍拼后续木棒显然比用一根长木棍拼更优,后者可达的状态是前者的真子集
Mayan游戏
交换操作具有对称性,即\(swap(A, B)\)和\(swap(B, A)\)的效果是一样的
所以当我们交换两个方块时,只需选择其中一个方块去操作即可,而为了使操作序列字典序最小,如果存在右边的方块,就只让该方块与左边交换,继续搜索即可,
即仅当右边没有方块时才将该方块向左交换
Missile Defending System
对于每一个导弹,有以下四种选择:
- 接在一个上升系统末尾
- 新开一个上升系统
- 接在一个下降系统末尾
- 新开一个下降系统
对于\(1\),可以发现选择比当前高度小的系统中末尾高度最大的最优
\(Proof:\)
假设有两个上升系统,末尾高度分别为\(a\),\(b\),当前导弹高度为\(c\),且满足\(c > a > b\)
若将\(c\)接在\(b\)后,则对于后续的导弹\(d\),仅当\(d\in[a, +\infty)\),\(d\)可接在这两个系统后
反之,若接在\(a\)后,则对于后续的导弹\(d\),当\(d\in[b, +\infty)\),\(d\)可接在这两个系统后
显然,后者的适用范围比前者大,故后者更优
选择\(3\)同理
故我们将上升系统与下降系统分别存在两个数组中,每次从中选取合法的最大或最小系统,如果无法找到,就进行选择\(2\),\(4\),继续搜索
进一步的,我们发现上升数组是单调递减的(归纳),下降数组同理,故不需要查找,每次选第一个元素即可。
3. 可行性、最优性剪枝
当前状态已经不符合题意时或已经比已知最优解劣时,立即回溯。
通常加上对未来的估计,或上下界剪枝
生日蛋糕
- 针对搜索的每一个参数,推导出一个不等式。
- 当前体积、表面积\(+\)预估最小体积、表面积已经超过总体积或当前最优解,剪枝
- 将各个参数结合在一起,进行剪枝
具体的,从下往上枚举,设当前层数为\(d\),当前体积为\(V\),当前表面积为\(S\),第\(1\)到\(x\)层最小体积、表面积为\(v_x\),\(s_x\),上一层半径,高度分别为\(R, H\)
则,$$v_x = \sum^x_{i=1} i^3$$,$$s_x = \sum^x_{i=1} i^2$$
-
若\(S+s_{d-1}>\)已知最优解,回溯
-
若\(V+v_{d-1}>N\),回溯
-
当前半径
外层循环枚举\(r\),内层循环枚举\(h\),并倒序枚举
- 注意第\(1\)到\(d\)层实际表面积、体积分别为
经过上述放缩后,得到\(S_d > \frac{2V_d}{r_d} >\frac{2(N-V)}{R}\),这也可以用来估计,剪枝
4. 记忆化
暂无题目
深搜变形
1. IDDFS 迭代加深
答案的深度不大,避免在一条错误路径上一直走下去
2. IDA*
带评估函数的迭代加深,相当于在搜索中加入对未来代价的估计,以便剪枝
当前深度+预估代价>限制深度 ==> 回溯
Square Destroyer
预处理出所有可能的正方形,然后从最小的正方形开始删除。
因为大正方形与小正方形要么相包含,内接,外接,相离。
对于每种情况,如果通过删除大正方形的一边能删除小正方形,则该边一定是两者的公共边,故也可选择小正方形达到同样的效果。
评估函数定义为:从当前状态开始,依次删除存在的正方形的每一条边,但只记为一次操作,直到所有正方形都被删除的操作数。
3. 双向DFS
多见于枚举方案型搜索,通过将数组折半进行优化
- 将数组按下标分为两段 \([1, mid]\),\([mid+1, n]\)
- 对第一段进行搜索,用一个新数组\(A\)记录所有最终能到达的状态
- 对\(A\)排序,去重
- 对第二段进行搜索,每次到达边界时,在\(A\)中二分查找合适的状态,与第二次搜索到的状态合并,得到完整状态
有些难懂 (-_-b)
送礼物
首先将礼物数组分成\([1, n/2]\)和\([n/2+1, n]\)两段。
然后在第一段中搜索出选择若干礼物重量和小于\(W\)的所有重量和,存入数组\(A\)中,并对\(A\)排序,去重。
接着搜索第二段,假设搜索到选择一些礼物的重量和为\(x\),则可以在\(A\)中二分查找小于\(W-x\)的最大值,加起来,更新答案
时间复杂度为$$O(2^{n/2} + 2{n/2}\lg2)=O(n2^{n/2})$$
其他技巧
位运算优化
数独中用位运算记录一个位置能填的数字
预处理
提前处理出可能的搜索对象,或要用的数据,简化代码,提升效率
预处理出一定范围内二进制数\(1\)位个数,预处理\(\log_2 x\)
选取搜索对象
Square Destroyer
并非枚举删除哪根木棍,而是枚举删除哪个正方形,再枚举删除其上的那一根木棍
The Buses
感觉题目没说明白
真正题意:每一条公交线路都是一个等差数列,且是数据中极大的等差数列(不能再添加新的项)
那么我们可以枚举每一条可能的公交线路(等差数列)看能否不重不漏覆盖数据的每一项(而非枚举每一辆到站的公交车,再选取它所属的线路)
其中,可以预处理出所有可能的等差数列,按长度从大到小排序,再进行搜索。
预处理时,首项\(i\)只需枚举\([0, 30]\)即可,若\(i > 30\),则公差\(d < 30\),则\(i\)之前必定还有一项,重复了。
而且,由于线路最多仅有\(17\)条,而等差数列最多可能有\(30\times60=1800\)个,故可以采用IDDFS
注意:同一等差数列可选多次
可行性剪枝:\(当前已覆盖数+(lim-dep)\times 当前数列项数<n\),因为数列长度是从大到小排序的,故如果深度限制内剩余线路全部长度为当前长度还不够,回溯
为了方便操作,可以用桶存储到站时间
BFS
每次只会在搜索树上拓展一层,不会一条路走到死,适用于走地图等问题
普通广搜队列具有两段性和单调性
广搜变形
这些变形都是为了维护广搜队列的两个性质
1. 双端队列BFS
电路维修
图论建模:将每个格点看作一个节点,如果有导线直接连接两点,则边权为\(0\),说明不需旋转
否则为\(1\),说明需要一次旋转
从左上角开始搜索即可,直到右下角第一次出队,返回最小旋转次数
2. 优先队列BFS
适用于边权为任意非负数的图,类似Dijkstra
对比
| 名称 | 使用条件 | 是否重复入队 | 何时搜到答案 | 时间复杂度 |
|---|---|---|---|---|
| BFS | 边权全为常数 | 否 | 第一次访问到/第一次出队 | \(O(N)\) |
| 双端队列BFS | 边权为\(0/1\)(或两个不同常数) | 是 | 第一次出队 | \(O(N)\) |
| 优先队列BFS | 边权为非负整数 | 是 | 第一次出队 | \(O(N\lg N)\) |
毒瘤题目 推箱子
双重BFS,通过记录状态前驱复原出整个操作序列
定义状态为\((x, y, dir)\),\(x, y\)表示箱子的位置,\(dir\)表示箱子的朝向(上一步推的方向),即人在该方向的反方向上
#include <cstdio>
#include <queue>
#include <stack>
#include <string>
using namespace std;
int n, m;
char map[25][25];
bool vis[25][25][4]; // 访问标记
int from[25][25][4]; // 上一个状态的dir
struct inf {
int x, y, d;
int bs, ms;
int ld;
};
bool operator <(inf a, inf b)
{
if (a.bs == b.bs)
return a.ms > b.ms;
return a.bs > b.bs;
}
pair<int, int> mst, bst, ed;
struct walk {
int x, y, dis;
};
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
char dirc[5] = "NSWE";
queue<walk> qm;
bool vism[25][25];
int fman[25][25];
int move(int sx, int sy, int tx, int ty, int bx, int by)
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
vism[i][j] = 0;
fman[i][j] = -1;
}
while (!qm.empty()) qm.pop();
if (tx < 1 || tx > n || ty < 1 || ty > m || map[tx][ty] == '#') return -1;
if (sx == tx && sy == ty) return 0;
qm.push((walk) {sx, sy, 0});
while (!qm.empty()) {
walk u = qm.front();
qm.pop();
for (int d = 0; d < 4; d++) {
int xv = u.x + dx[d], yv = u.y + dy[d];
if (xv < 1 || xv > n || yv < 1 || yv > m || map[xv][yv] == '#' || (xv == bx && yv == by)) continue;
if (vism[xv][yv]) continue;
vism[xv][yv] = 1;
fman[xv][yv] = d;
if (xv == tx && yv == ty) {
return u.dis + 1;
}
qm.push((walk) {xv, yv, u.dis + 1});
}
}
return -1;
}
// 优先队列BFS,保证在箱子步数递增的基础上,人的步数递增
priority_queue<inf> q;
int bfs(void)
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int d = 0; d < 4; d++) {
from[i][j][d] = -1;
vis[i][j][d] = 0;
}
while (!q.empty()) q.pop();
for (int d = 0; d < 4; d++) { // 在初始状态后再移动一步,不然如果只是将人移动到某一方向对应位置,后续更新时无法保证字典序最优
int xv = bst.first - dx[d], yv = bst.second - dy[d];
int bx = bst.first + dx[d], by = bst.second + dy[d];
if (bx < 1 || bx > n || by < 1 || by > m || map[bx][by] == '#') continue;
int ret = move(mst.first, mst.second, xv, yv, bst.first, bst.second);
if (ret != -1) {
q.push((inf) {bx, by, d, 1, ret, -1});
}
}
while (!q.empty()) {
inf u = q.top();
q.pop();
if (vis[u.x][u.y][u.d]) continue;
vis[u.x][u.y][u.d] = 1;
from[u.x][u.y][u.d] = u.ld;
if (u.x == ed.first && u.y == ed.second)
return u.d;
for (int d = 0; d < 4; d++) {
int bxv = u.x + dx[d], byv = u.y + dy[d];
if (bxv < 1 || bxv > n || byv < 1 || byv > m || map[bxv][byv] == '#') continue;
int mxv = u.x - dx[d], myv = u.y - dy[d];
int oxv = u.x - dx[u.d], oyv = u.y - dy[u.d];
int ret = move(oxv, oyv, mxv, myv, u.x, u.y);
if (ret != -1) {
q.push((inf) {bxv, byv, d, u.bs + 1, u.ms + ret, u.d});
}
}
}
return -1;
}
stack<char> out;
void print_path(int dir)
{
int bx = ed.first, by = ed.second;
while (dir != -1) { // 不能写成 bx != mst.first && by != mst.second,因为有些数据会重复经过同一位置,但方向不同
int xv = bx - dx[dir], yv = by - dy[dir];
int ld = from[bx][by][dir];
int msx = ld == -1 ? mst.first : (xv - dx[ld]), msy = ld == -1 ? mst.second : (yv - dy[ld]);
int mtx = xv - dx[dir], mty = yv - dy[dir];
out.push(dirc[dir]);
move(msx, msy, mtx, mty, xv, yv);
int md = fman[mtx][mty];
while (msx != mtx || msy != mty) {
out.push(dirc[md] - 'A' + 'a');
mtx -= dx[md];
mty -= dy[md];
md = fman[mtx][mty];
}
bx = xv, by = yv;
dir = ld;
}
while (!out.empty()) {
putchar(out.top());
out.pop();
}
puts("");
}
int main(void)
{
int id = 0;
while (scanf("%d %d", &n, &m) == 2 && (n || m)) {
for (int i = 1; i <= n; i++)
scanf("%s", map[i] + 1);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
if (map[i][j] == 'S') mst = {i, j};
else if (map[i][j] == 'B') bst = {i, j};
else if (map[i][j] == 'T') ed = {i, j};
}
printf("Maze #%d\n", ++id);
int x = bfs();
if (x == -1) puts("Impossible.");
else print_path(x);
puts("");
}
return 0;
}
3. A*
A*=优先队列BFS+估价函数
注意满足估价<实际,不然会卡在堆里出不来
一些估价函数:
- 与目标状态不一样的位置数 The Rotation Game 骑士精神
- 到目标的曼哈顿距离(之和) 八数码
- 地图中数字种类数 Flood-it!
- \(\lg \frac{p}{max(a, b)}\) Power Hungry Cow

浙公网安备 33010602011771号