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