搜索

搜索 = “暴力法”

1.dfs

\(dfs\) 的工作原理就是递归,即“\(dfs\) = 递归”。在搜索算法中,该词常常指利用递归函数方便地实现暴力枚举的算法,与图论中的 \(dfs\) 算法有一定相似之处,但并不完全相同。

下面给出 \(dfs\) 的基本代码框架:

void dfs(层数, 其他参数){
	if(结束判断){
		更新答案;
		return; 
	}
	(剪枝)
	for(枚举下一层可能的情况){
		if(!vis[i]){	//vis为标记数组
			vis[i] = true;
			dfs(层数 + 1, 其他参数)
			vis[i] = false;
		}
	}
}

再给出一道 \(dfs\) 例题,以更好地理解 \(dfs\)
题目:luogu P1219

看到数据范围立刻想到 \(dfs\),枚举每种可能性。

展示代码,带注释:

#include<bits/stdc++.h>
#define N 23
using namespace std;
int n, ans;
int r[N];
bool c[N], ld[N], rd[N];
//r表示行,c表示列是否被占领
//lr表示从右上到左下的对角线是否被占领,rd表示从左上到右下的对角线是否被占领
void print(){
	if(ans <= 2){	//方案输出前三个
		for(int i = 1; i <= n; i++){
			printf("%d ", r[i]);
		}
		puts("");
	}
	ans++;
}
void dfs(int x){
	if(x > n){	//判结束
		print();
		return;
	}
	for(int i = 1; i <= n; i++){	// 尝试可能的位置
		if(!c[i] && !ld[x + i] && !rd[x - i + n]){	//如果列, 两条对角线皆没有皇后
			r[x] = i;
			c[i] = true;
			ld[x + i] = true;
			rd[x - i + n] = true;	//标记 
			dfs(x + 1);	//继续搜索下一行
			c[i] = false;
			ld[x + i] = false;
			rd[x - i + n] = false;	//回溯
		}
	}
}
int main(){
	scanf("%d", &n);
	dfs(1);	//搜索
	printf("%d", ans);
	return 0;
}

2.bfs

\(bfs\),即广度优先搜索。所谓宽度优先,就是指每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。我们用队列记录访问的节点(先进先出)。

下面给出普通 \(bfs\) 的代码框架:

bfs(s){
	q.push(s);
	vis[s] = true;	//vis为标记数组 
	while(!q.empty()){
		u = q.top();
        q.pop();
		for each node(u, v){	//枚举每个u的邻居 
			if(!vis[v]){	//枚举每个邻居 
			    q.push(v);
			    vis[v] = true;
			}
		}
	}
}

这里再给出一道 \(bfs\) 例题,可以更好的理解 \(bfs\)
题目:luogu P1144

在这道题中,因为所有的边权都为 \(1\),所以一个点的最短路就相当于是它在 \(bfs\) 搜索树中的深度。一个点的最短路一定经过了一个层数比它少一的结点(否则不是最短路),所以用每个相邻且层数比当前结点层数少一的点更新当前点的路径数即可。

展示代码:

#include<bits/stdc++.h>
#define N 1000010
#define mod 100003
using namespace std;
int n, m, u, v;
int d[N], ans[N];
bool vis[N];
vector<int>a[N];
queue<int>q;
void bfs(int s){
	q.push(s);
	vis[s] = true;	//打标记
	d[s] = 0;	//d[i]为从s到i的距离
	ans[s] = 1;	//ans[i]表示从s到i有几条不同的最短路
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(int i = 0; i < a[u].size(); i++){
			int v = a[u][i];
			if(!vis[v]){	//如果没访问过
				vis[v] = true;	//打上标记
				d[v] = d[u] + 1;	//记录距离
				q.push(v); 
			}
			if(d[v] == d[u] + 1){	//如果刚被访问
				ans[v] += ans[u];	//记录ans
				ans[v] %= mod;	//记得模mod
			}
		}
	}
}
int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; i++){
		scanf("%d%d", &u, &v);
		a[u].push_back(v);	//邻接表存图
		a[v].push_back(u);
	}
	bfs(1);	//搜索
	for(int i = 1; i <= n; i++){
		printf("%d\n", ans[i]); 
	}
	return 0;
}

3.双向bfs

双向 \(bfs\) 是指双向广搜,他的基本思路就是从图上的起点和终点同时进行 \(bfs\),如果发现搜索的两端相遇了,就可以认为是获得了可行解。

它的大致结构是这样的:

将开始结点和目标结点加入队列 q
标记开始结点为 1
标记目标结点为 2
while (队列 q 不为空){
	从 q.front() 扩展出新的 x 个结点
	如果 新扩展出的结点已经被其他数字标记过
		那么 表示搜索的两端碰撞
		那么 循环结束
	如果 新的 x 个结点是从开始结点扩展来的
		那么 将这个 x 个结点标记为 1 并且入队 q 
	如果 新的 x 个结点是从目标结点扩展来的
		那么 将这个 x 个结点标记为 2 并且入队 q
}

有一道非常经典的双向 \(bfs\) 题:字串变换 luogu P1032

这道题很明显要用 \(bfs\)(如果用 \(dfs\) 就不能保证是最小步数了),但如果是纯 \(bfs\),时间复杂度为 \(O((20 * 6) ^ {10})\)(字符串长度最长为 \(20\),至多 \(6\) 个规则,步数为 \(10\))。
但如果用双向 \(bfs\),起点走 \(5\) 步,终点走 \(5\) 步,时间复杂度为 \(O(2 * (20 * 6) ^ 5)\),大大降低。
展示一下代码:

#include<bits/stdc++.h>
using namespace std;
int n, ans;
string aa, bb;
string a[20], b[20];
int extend(queue<string>&q, unordered_map<string, int>&ma, unordered_map<string, int>&mb, string a[], string b[]){
	string A = q.front();	//取出队头 
	int mA = ma[A];
	while(!q.empty()){
		A = q.front();
		if(ma[A] != mA){
			break;
		}
		q.pop();
		for(int i = 0; i < A.size(); i++){
			for(int j = 0; j < n; j++){
				if(A.substr(i, a[j].size()) == a[j]){
					string B = A.substr(0, i) + b[j] + A.substr(i + a[j].size());
					if(ma.count(B)){
						continue;
					}
					if(mb.count(B)){
						return ma[A] + mb[B] + 1;
					}
					ma[B] = ma[A] + 1;
					q.push(B);
				}
			}
		}
	}
	return 11;
}
int bfs(string aa, string bb){
	queue<string>qa, qb;	//一个从起点开始,一个从终点开始 
	unordered_map<string, int>ma, mb;	//用于判重 
	qa.push(aa);	//初始化 
	qb.push(bb);
	ma[aa] = 0;
	mb[bb] = 0;
	int step = 0;
	while(!qa.empty() && !qb.empty()){
		int t;
		if(qa.size() < qb.size()){
			t = extend(qa, ma, mb, a, b);	//运用规则 
		}
		else{
			t = extend(qb, mb, ma, b, a);	//要不然反着用 
		}
		if(t <= 10){
			return t;
		}
		step++;
		if(step == 10){
			return -1;
		}
	}
	return -1;
}
int main(){
	cin >> aa >> bb;
	while(cin >> a[++n] >> b[n]){
	}
	ans = bfs(aa, bb);
	if(ans != -1){
		printf("%d", ans);
	}
	else{
		puts("NO ANSWER!");
	}
	return 0;
}

4.剪枝

剪枝是搜索必用的优化手段,它可以大大降低普通搜索的复杂度。

\(dfs\) 的常用剪枝技术,有:

1)可行性剪枝

对当前状态进行检查,如果当前条件不合法就不再继续。

2)最优性剪枝

在最优化问题的搜索过程中,如果当前花费的代价已经超过以前搜索的最优解,就直接退出(因为
已经没有意义)。

3)记忆化搜索

在递归的过程中,可以将计算出来的结果保存起来,以后要用到时就可以直接取出结果,避免了重复运算。
\(bfs\) 的剪枝技术通常是判重,如果搜索到某一层时,出现了重复的状态,就“剪掉”。

给出一道剪枝例题:luogu P1025
代码:

#include<bits/stdc++.h>
using namespace std;
int n, k, ans;
void dfs(int now, int s, int b){	//now表示上一个分的数是几,s表示和,b表示有几部分 
	if(b == k){
		if(s == n){
			ans++;
		}
		return;
	}
	for(int i = now; i <= n; i++){	//剪枝,如果只加i都会超过n,那就不行 
		dfs(i, s + i, b + 1);
	}
}
int main(){
	scanf("%d%d", &n, &k);
	dfs(1, 0, 0);
	printf("%d", ans);
	return 0;
}

5.A*

是一种在图形平面上,对于有多个节点的路径求出最低通过成本的算法。它属于图遍历和贪心最优算法,亦是 \(bfs\) 的改进。
在进行此算法的过程中,会定义从起点开始的距离函数 \(g(x)\),到终点的距离函数 \(h(x)\)\(h\)*\((x)\),以及每个点的估价函数 \(f(x) = g(x) + h(x)\)

\(A\)*算法每次从优先队列中取出一个f最小的元素,然后更新相邻的状态。

如果 \(h\) \(\leq\) \(h\)*,则 \(A\)*算法能找到最优解。当 \(h = 0\) 时,\(A\)*算法变为 \(dijkstra\),而当 \(h = 0\) 并且边权为1时变为 \(bfs\)

给出一道 \(A\)*例题:luogu P1379(八数码难题)
在这道题中 \(h\) 函数可以定义为,不在应该在的位置的数字个数。容易发现 \(h\) 满足以上两个性质,此题可以使用 \(A\)*算法求解。
给出代码:

#include<bits/stdc++.h>
using namespace std;
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, 1, -1};
int kx, ky;
char c;
struct matrix {
  	int a[4][4];
  	bool operator < (matrix x) const{
	    for (int i = 1; i <= 3; i++){
	    	for (int j = 1; j <= 3; j++){
	    		if (a[i][j] != x.a[i][j]){
	    			return a[i][j] < x.a[i][j];
				}
			}
		}
	    return false;
	}
}f, st;
int h(matrix a){	//比较函数 
  	int res = 0;
  	for(int i = 1; i <= 3; i++){
  		for(int j = 1; j <= 3; j++){
			if(a.a[i][j] != st.a[i][j]){
				res++;
			}
		}  		
	}	
	return res;
}
struct node{
	matrix a;
	int t;
	bool operator < (node x) const{
		return t + h(a) > x.t + h(x.a);
	}
}x;
priority_queue<node>q;	//搜索队列
set<matrix>s;	//set去重 
int main(){
	st.a[1][1] = 1;	//处理出合理序列 
	st.a[1][2] = 2;
	st.a[1][3] = 3;
	st.a[2][1] = 8;
	st.a[2][2] = 0;
	st.a[2][3] = 4;
	st.a[3][1] = 7;
	st.a[3][2] = 6;
	st.a[3][3] = 5;
	for(int i = 1; i <= 3; i++){
		for(int j = 1; j <= 3; j++){
			scanf("%c", &c);
			f.a[i][j] = c - '0';
		}
	}
	q.push({f, 0});
	while(!q.empty()){
		x = q.top();
		q.pop();
		if(!h(x.a)){
			printf("%d", x.t);
			return 0;
		}
		for(int i = 1; i <= 3; i++){
			for(int j = 1; j <= 3; j++){
				if(!x.a.a[i][j]){
					kx = i;
					ky = j;  //查找空格子(0号点)的位置
				}
			}
		}
		for(int i = 0; i < 4; i++){  //对四种移动方式分别进行搜索
	      	int xx = kx + dx[i];
			int yy = ky + dy[i];
	      	if(1 <= xx && xx <= 3 && 1 <= yy && yy <= 3){
	        	swap(x.a.a[kx][ky], x.a.a[xx][yy]);
		        if(!s.count(x.a)){	//是否出现过 
		        	s.insert(x.a);
		        	q.push({x.a, x.t + 1});  //这样移动后,将新的情况放入搜索队列中
				}
	        	swap(x.a.a[kx][ky], x.a.a[xx][yy]);  //如果不这样移动的情况
		    }
	    }
	}
	return 0;
}

6.IDDFS

\(IDDFS\),即迭代加深搜索,它的本质还是 \(dfs\),只不过在搜索的同时带上了一个深度 \(d\),当 \(d\) 达到设定的深度时就返回,一般用于找最优解。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始。
这里只给出 \(IDDFS\) 的伪代码:

IDDFS(u, d)
    if d > limit  //limit为限定的深度
        return
    for each edge (u, v)
        IDDFS(v, d + 1)

7.IDA*

\(IDA\)*,即采用了迭代加深算法的 \(A\)*算法。
由于 \(IDA\)*改成了深度优先的方式,相对于 \(A\)*算法,它的优点如下:

  • 1)不需要判重,不需要排序,利于深度剪枝。
  • 2)空间需求减少:每个深度下实际上是一个深度优先搜索,不过深度有限制,使用 \(dfs\) 可以减小空间消耗。
    有优有缺,缺点:
    • 1)重复搜索:即使前后两次搜索相差微小,回溯过程中每次深度变大都要再次从头搜索。

在给出一道可以用 \(IDA\)* 做的题:luogu P1379(八数码难题)

对,这道题也可以用 \(IDA\)*做!

在这里我们的A*估价函数设置为当前状态还有多少个位置与目标状态不对应,若当前步数 + 估价函数值 > 枚举的最大步数,则直接返回。

当然这只是基本思路,搜索还可以有很大优化,我们在搜索中再加入最优性剪枝, 显然当前枚举下一个状态时如果回到上一个状态肯定不是最优, 所以我们在枚举下一状态时加入对这种情况的判断。

AC代码:

#include<bits/stdc++.h>
using namespace std;
const int bz[4][4] = {
	{0, 0, 0, 0},
	{0, 1, 2, 3},
	{0, 8, 0, 4},
	{0, 7, 6, 5}
};
const int dx[] = {0, 1, -1, 0};
const int dy[] = {1, 0, 0, -1};
char s[20];
int x, y, ans;
int a[5][5];
bool judge;
bool check(){
	for(int i = 1; i <= 3; i++){
		for(int j = 1; j <= 3; j++){
			if(a[i][j] != bz[i][j]){
				return false;
			}
		}
	}
	return true;
}
bool check2(int step){
	int cnt = 0;
	for(int i = 1; i <= 3; i++){
		for(int j = 1; j <= 3; j++){
			if(a[i][j] != bz[i][j]){
				if(++cnt + step > ans){
					return false;
				}
			}
		}
	}
	return true;
}
void a_star(int step, int x, int y, int pre){
	if(step == ans){	//到达限制深度 
		if(check()){
			judge = true;
		}
		return;
	}
	if(judge){
		return;
	}
	for(int i = 0; i < 4; i++){
		int X = x + dx[i];
		int Y = y + dy[i];
		if(X < 1 || X > 3 || Y < 1 || Y > 3 || pre + i == 3){	//最优性剪枝 
			continue;
		}
		swap(a[x][y], a[X][Y]);
		if(check2(step) && !judge){
			a_star(step + 1, X, Y, i);
		}
		swap(a[x][y], a[X][Y]);
	}
}
int main(){
	scanf("%s", s);
	for(int i = 0; i < 9; i++){
		a[i / 3 + 1][i % 3 + 1] = s[i] - '0';
		if(s[i] == '0'){
			x = i / 3 + 1;
			y = i % 3 + 1;
		}
	}
	if(check()){	//特判不用动 
		printf("0");
		return 0;
	}
	while(++ans){
		a_star(0, x, y, -1);
		if(judge){
			printf("%d", ans);
			break;
		}
	}
	return 0;
}
posted @ 2023-09-10 15:55  lijingqian  阅读(11)  评论(0编辑  收藏  举报