搜索中的判重(以BFS为例)

预告:我用两年写的新书《算法竞赛》,已于2022年2月交给清华大学出版社,预计于2022年7月出版。《算法竞赛》是一本“大全”,内容覆盖“基础-中级-高级”,篇幅700页左右。部分知识点的草稿已经在本博客发表。本篇博客节选自新书《算法竞赛》的“3.2.1 BFS判重”。


   判重,即判断当前状态是否以前已经处理过。如果已经处理过,就不用再处理了,从这个角度看,判重是一种 剪枝技术。
   判重常常应用在BFS中,BFS剪枝的题目很多需要判重。
   BFS的原理是逐步扩展下一层,把扩展出的下一层点放进队列中处理。在处理上一层的同时,把下一层的点放到队列的尾部。在任意时刻,队列中只包含相邻两层的点。如果这些点都不同,只能把所有点放进队列。如果这些点有相同的,那么相同的点只处理一次就够了,其他相同的点不用重复处理,此时需要判重。
   下面的真题是BFS判重。


2017年蓝桥杯省赛真题
跳蚱蜢 https://www.lanqiao.cn/problems/642/learning/
题目描述:有9只盘子,排成1个圆圈。其中8只盘子内装着8只蚱蜢,有一个是空盘。
把这些蚱蜢顺时针编号为 1~8。每只蚱蜢都可以跳到相邻的空盘中,也可以再用点力,越过一个相邻的蚱蜢跳到空盘中。
请你计算一下,如果要使得蚱蜢们的队形改为按照逆时针排列,并且保持空盘的位置不变(也就是1-8换位,2-7换位,…),至少要经过多少次跳跃?


   这是一道八数码问题,八数码是经典的BFS问题。
   本题首先用了“化圆为线”的技巧。直接让蚱蜢跳到空盘有点麻烦,因为有很多蚱蜢在跳。如果反过来看,让空盘跳,跳到蚱蜢的位置,就简单多了,只有一个空盘在跳。题目给的是一个圆圈,不好处理,可以“化圆为线”。把空盘看成0,那么有9个数字{0,1,2,3,4,5,6,7,8},一个圆圈上的9个数字,拉直成了一条线上的9个数字。这就是八数码问题,八数码有9个数字{0,1,2,3,4,5,6,7,8},它有9!=362880种排列,不算多。
   本题的初始状态是“012345678”,终止状态是“087654321”。从初始状态跳一次,下一状态有4种情况,如图所示。

在这里插入图片描述
   用BFS扩展每一层。每一层就是蚱蜢跳了一次,扩展到某一层时发现终点“087654321”,这一层的深度就是蚱蜢跳跃的次数。
   所以,八数码问题实际是一个最短路径问题,用BFS最合适。
   这题如果写个裸的BFS,能运行出来吗?第1步到第2步,有4种跳法;第2步到第3步,有 4 2 4^2 42种;…;第20步,有 4 20 4^{20} 420 = 1万亿种。
   必须判重,判断有没有重复跳,如果跳到一个曾经出现过的情况,就不用往下跳了。一共只有9!= 362880种情况。代码的复杂度是多少?在每一层,能扩展出最少4种、最多362880种情况,最后算出的答案是20层,那么最多算20*362880 = 7,257,600次。在下面的C++代码中统计实际的计算次数,是1451452次。
   如何判重?用STL的map、set判重,效率都很好。
   另外有一种数学方法叫康托判重(康托判重的详细讲解,参考《算法竞赛入门到进阶》,清华大学出版社,罗勇军,郭卫斌著,“4.3.2 八数码问题”。),竞赛时一般不用。
   下面是“跳蚱蜢”的代码,有map和set两种判重方法。请自己了解STL map和set的概念。

1. map判重

#include<bits/stdc++.h>
using namespace std;
struct node{
	node(){}
	node(string ss, int tt){s = ss, t = tt;}
	string s;
	int t;
};
//(1) map
map<string, bool> mp;

queue<node> q;
void solve(){
	while(!q.empty()){
		node now = q.front();
		q.pop();
		string s = now.s;
		int step = now.t;
		if(s == "087654321"){ cout<<step<<endl; break;}   //到目标了,输出跳跃步数
		int i;
		for(i = 0 ; i < 10 ; i++)               //找到盘子的位置i
		    if(s[i] == '0')  break;
		for(int j = i - 2 ; j <= i + 2 ; j++){  //4种跳法
		    int k = (j + 9) % 9;
		    if(k == i)	continue;               //这是当前状态,不用检查
		    string news = s;
		    char tmp = news[i];
             news[i] = news[k];
             news[k] = tmp;  //跳到一种情况
//(1) map
			if(!mp[news]){                 //判重:这个情况没有出现过
				mp[news] = true;
				q.push(node(news, step + 1));
			}
		}
	}
}
int main(){
	string s = "012345678";
	q.push(node(s, 0));
//(1) map
	mp[s] = true;
	solve();
	return 0;
}

2. set判重

#include<bits/stdc++.h>
using namespace std;
struct node{
	node(){}
	node(string ss, int tt){s = ss, t = tt;}
	string s;
	int t;
};

//(2) set
set<string> visited;    //记录已经搜索过的状态
queue<node> q;
void solve(){
	while(!q.empty()){
		node now = q.front();
		q.pop();
		string s = now.s;
		int step = now.t;
		if(s == "087654321"){ cout<<step<<endl; break;}   //到目标了,输出跳跃步数
		int i;
		for(i = 0 ; i < 10 ; i++)               //找到盘子的位置i
		    if(s[i] == '0')  break;
		for(int j = i - 2 ; j <= i + 2 ; j++){  //4种跳法
		    int k = (j + 9) % 9;
		    if(k == i)	continue;               //这是当前状态,不用检查
		    string news = s;
		    char tmp = news[i];
             news[i] = news[k];
             news[k] = tmp;  //跳到一种情况
//(2)set
        if(visited.count(news)==0){    //判重:这个情况没有出现过
				visited.insert(news);
				q.push(node(news, step + 1));
            }
		}
	}
}
int main(){
	string s = "012345678";
	q.push(node(s, 0));
	solve();
	return 0;
}
  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗勇军

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值