搜索

搜索题的典型特征:通常数据范围较小,时间复杂度为指数级

DFS

会在一条路上走到底再回溯,采用递归形式

剪枝

1. 优化搜索顺序

小木棍

将木棒长度排序,优先考虑长的木棒

数独问题 (数独数独2靶形数独

枚举未填格子时,优先考虑能填数字最少的格子

虫食算

从竖式的最右端开始搜索,这样可以使三个数字均已填出的竖列更多,提升剪枝效率

2. 排除等效冗余

小木棍

  1. 限制同一根木棒中木棍长度递减,不走回头路,排列 -> 组合
  2. 存在相等数据:记录失败数据,避免重复尝试
  3. 如果当前木棒尝试拼入第一根木棍A就失败,则立即回溯。因为几根没有拼的木棒是等价的,无论木棍A拼入哪一根都会失败,不然我们只需调整该成功方案中木棒顺序,就可得到使本次搜索成功的方案。
  4. 如果一根木棒刚好拼完,但后续搜索失败,立即回溯。因为用若干根小木棍拼后续木棒显然比用一根长木棍拼更优,后者可达的状态是前者的真子集

Mayan游戏

交换操作具有对称性,即\(swap(A, B)\)\(swap(B, A)\)效果是一样的
所以当我们交换两个方块时,只需选择其中一个方块去操作即可,而为了使操作序列字典序最小,如果存在右边的方块,就只让该方块与左边交换,继续搜索即可,
即仅当右边没有方块时才将该方块向左交换

Missile Defending System

对于每一个导弹,有以下四种选择:

  1. 接在一个上升系统末尾
  2. 新开一个上升系统
  3. 接在一个下降系统末尾
  4. 新开一个下降系统

对于\(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. 可行性、最优性剪枝

当前状态已经不符合题意时或已经比已知最优解劣时,立即回溯。
通常加上对未来的估计,或上下界剪枝

生日蛋糕

  1. 针对搜索的每一个参数,推导出一个不等式。
  2. 当前体积、表面积\(+\)预估最小体积、表面积已经超过总体积或当前最优解,剪枝
  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$$

  1. \(S+s_{d-1}>\)已知最优解,回溯

  2. \(V+v_{d-1}>N\),回溯

  3. 当前半径

\[r\in \left[d, \min(\sqrt{\frac{N-V-v_{d - 1}}{d}}, R - 1) \right] \]

外层循环枚举\(r\),内层循环枚举\(h\),并倒序枚举

\[h \in \left[d, \min(\frac{N-V-v_{d - 1}}{r^2}, H - 1) \right] \]

  1. 注意第\(1\)\(d\)层实际表面积、体积分别为

\[S_d = \sum^d_{i=1} 2r_i h_i \]

\[\begin{align*} V_d &= \sum^d_{i=1} r_i^2 h_i \\ &< \sum^d_{i=1} r_d r_i h_i = r_d \sum^d_{i=1} r_i h_i = r_d \times \frac{S_d}{2} \end{align*} \]

经过上述放缩后,得到\(S_d > \frac{2V_d}{r_d} >\frac{2(N-V)}{R}\),这也可以用来估计,剪枝

4. 记忆化

暂无题目

深搜变形

1. IDDFS 迭代加深

答案的深度不大,避免在一条错误路径上一直走下去

2. IDA*

带评估函数的迭代加深,相当于在搜索中加入对未来代价的估计,以便剪枝
当前深度+预估代价>限制深度 ==> 回溯

Square Destroyer

预处理出所有可能的正方形,然后从最小的正方形开始删除。
因为大正方形与小正方形要么相包含,内接,外接,相离。
对于每种情况,如果通过删除大正方形的一边能删除小正方形,则该边一定是两者的公共边,故也可选择小正方形达到同样的效果。

评估函数定义为:从当前状态开始,依次删除存在的正方形的每一条边,但只记为一次操作,直到所有正方形都被删除的操作数。

3. 双向DFS

多见于枚举方案型搜索,通过将数组折半进行优化

  1. 将数组按下标分为两段 \([1, mid]\)\([mid+1, n]\)
  2. 对第一段进行搜索,用一个新数组\(A\)记录所有最终能到达的状态
  3. \(A\)排序,去重
  4. 对第二段进行搜索,每次到达边界时,在\(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})$$

其他技巧

位运算优化

数独中用位运算记录一个位置能填的数字

预处理

提前处理出可能的搜索对象,或要用的数据,简化代码,提升效率

Square DestroyerThe Buses

预处理出一定范围内二进制数\(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+估价函数

注意满足估价<实际,不然会卡在堆里出不来

一些估价函数:

最后,多测记得清空!多测记得清空!!多测记得清空!!!

posted @ 2025-08-26 21:23  zhm0725  阅读(20)  评论(0)    收藏  举报