发现自己对搜索的了解还是太浅了,决定把八数码彻底搞懂
题目大意
题目链接
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
显然这是一道搜索的题,搜索的对象是每一次矩形的状态,难点在于我们要避免重复的状态出现,不然反手就是一个stack overflow。直接将这 9 个数字凑成 9 位数再用 bool数组去重 绝对是不行的(1e9还不炸内存吗)既然是学算法,不如从这道题这里开始,把搜索从头到尾过一遍。ps:这种题不建议使用深度搜索(启发式搜索除外),因为你可能陷入局部循环(((φ(◎ロ◎;)φ)))。
一、基础bfs:矩阵转数字+map去重
我们将每一个矩阵转化成9位的数字来存储并判断是否出现了重复状态,首先八数码的矩阵用9个int有点浪费空间9个数字都是0到9,可以用一个存就行了分别放到每一个数位里在矩阵和int之间转化就行了,对于去重,我们既然把他转化成了一个数字,当然可以直接用map来进行记录,将状态映射到step上。注意这里为了保证转化的正确,矩阵转数字是从左上角到右下角,但是数字转矩阵需要从右下角到左上角。考虑012345678,我们存储到数字是12345678,如果从左上角开始,s[0][0]=1
显然不对。
注意我们使用map.count(x)
,来寻找map种是否出现了x这个元素,如果出现了就返回1,否则返回0.
#include<iostream>
#include<string>
#include<map>
#include<string.h>
#include<queue>
using namespace std;
#define ll long long
map<ll, ll> state; // state映射到步数
ll dx[4] = { 0,0,1,-1 }, dy[4] = { 1,-1,0,0 };
int main() {
ll a, s;
cin >> s;
queue<ll> q; q.push(s);
state[s] = 0;
while (!q.empty()) {
ll u = q.front(); q.pop();
ll c[3][3], f = 0, g = 0, n = u;
if (u == 123456780)break;
for(int i=2;i>=0;i--)
for (int j = 2; j >= 0; j--) {
c[i][j] = n % 10; n /= 10;
if (!c[i][j]) { f = i, g = j; }//记录0在哪里
}
for (int i = 0; i < 4; i++) {
ll x = dx[i] + f, y = dy[i] + g, ns = 0;
if (x < 0 || x>2 || y < 0 || y>2)continue;
swap(c[x][y], c[f][g]);
for (int i = 0; i < 3; i++)for (int j = 0; j < 3; j++)ns = ns * 10 + c[i][j];//新出现的状态转化为数字
if (!state.count(ns)) {//状态从没有出现过
state[ns] = state[u] + 1;
q.push(ns);
}
swap(c[x][y], c[f][g]);
}
}
cout << state[283104765] << endl;
return 0;
}
二、双向BFS+map去重
所谓双向搜索指的是搜索沿两个方向同时进行:
正向搜索:从初始结点向目标结点方向搜索;
逆向搜索:从目标结点向初始结点方向搜索;
当两个方向的搜索生成同一子结点时终止此搜索过程
双向BFS的使用要求就是知道终止状态也知道起始状态(一般知道这两个状态双向广搜就很稳了),这道题目实在是在合适不过了。 这里可以将判重数组的值设为0,1,2,分别代表未访问过,顺序访问过,逆序访问过,当某个状态被顺序逆序都访问过时,那么这就是连接答案的那个状态。
双向广搜实现起来也不复杂,使用一个队列将终止和起始状态存进去,然后正常的和上面一样进行搜索,不过需要注意的是,当新延伸出的状态已经被另一方向的搜索访问过时,正反搜索碰撞到了一个点上,这个点正反搜索用的总步数就是我们要求的结果。
#include<iostream>
#include<string>
#include<map>
#include<string.h>
#include<queue>
using namespace std;
#define ll long long
ll target[3][3] = { 1,2,3,8,0,4,7,6,5 };
ll s[3][3], judge = 0, k;
//将状态数组对称排列会很方便排除回到上一状态的情况
ll dx[4] = { 0,-1,1,0 }, dy[4] = { 1,0,0,-1 };
int main() {
ll ss, tt = 123804765;
cin >> ss;
if (ss == tt) { cout << 0 << endl; return 0; }
map<ll, ll> m, step;
queue<ll> q;
m[ss] = 1, m[tt] = 2;//1表示正向搜索,2表示逆向搜索
step[ss] = 0, step[tt] = 1;//step存储搜索的步数,注意末态开始搜索起始为1
q.push(ss); q.push(tt); //状态入队
while (!q.empty()) {
ll state = q.front(), c[3][3], f, d, tmp = state; q.pop();
//一定是从右下角到左上角复原 考虑012345678:如果从右上开始就会错
for (int i = 2; i >= 0; i--)
for (int j = 2; j >= 0; j--) {
c[i][j] = tmp % 10; tmp /= 10;
if (c[i][j] == 0) f = i, d = j;
}
for (int i = 0; i < 4; i++) {
ll xx = f + dx[i], yy = d + dy[i];
if (xx < 0 || xx>2 || yy < 0 || yy>2)continue;
swap(c[xx][yy], c[f][d]);
tmp = 0;
//从左上角到右下角将矩阵转化为数字
for (int i = 0; i < 3; i++)for (int j = 0; j < 3; j++)tmp = tmp * 10 + c[i][j];
if (m[tmp] == m[state]) {//转化后的状态和state一样同向搜索过
swap(c[xx][yy], c[f][d]);
continue;
}
if (m[tmp] + m[state] == 3) {//正反搜索碰面,说明新延伸出的点已被另一方向访问过
cout << step[tmp] + step[state] << endl;
return 0;
}
step[tmp] = step[state] + 1; //当前状态比上一状态多用一步
m[tmp] = m[state]; //当前状态和衍生出他的状态是同向的搜索结果
q.push(tmp);
swap(c[xx][yy], c[f][d]);
}
}
}
三、A*算法:最大限制的迭代加深搜素
A* 算法作为一种广为人知的启发式搜索算法,只要确定了启发式信息,他的搜索速度往往比普通的dfs来得更快。而且A*使用的是深度搜索的策略,这与前两种算法截然不同。
所谓迭代加深就是每次限制搜索深度, 这样可以在整个搜索树深度很大而答案深度又很小的情况下大大提高效率
使最大步数k从1开始不断加深枚举, 作为最大步数进行迭代加深搜索判断,而对于不用移动的情况可以一开始直接特判
在这里我们的A估价函数设置为
当前状态还有多少个位置与目标状态不对应
若当前步数+估价函数值>枚举的最大步数 则直接返回
当然这只是基本思路,搜索还可以有很大优化
我们在搜索中再加入最优性剪枝, 显然当前枚举下一个状态时如果回到上一个状态肯定不是最优, 所以我们在枚举下一状态时加入对这种情况的判断
我们将状态数组定义成如下形式,分别代表了右,下,上,左的策略,对上一搜索使用的策略i
,如果本轮使用第j
个策略,那么我们有如果i+j=3
,则表示回到了上一状态
//将状态数组对称排列会很方便排除回到上一状态的情况
ll dx[4] = { 0,-1,1,0 }, dy[4] = { 1,0,0,-1 };
#include<iostream>
#include<string>
#include<map>
#include<string.h>
#include<queue>
using namespace std;
#define ll long long
ll target[3][3] = { 1,2,3,8,0,4,7,6,5 };
ll s[3][3], judge = 0, k;
//将状态数组对称排列会很方便排除回到上一状态的情况
ll dx[4] = { 0,-1,1,0 }, dy[4] = { 1,0,0,-1 };
bool check() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (s[i][j] != target[i][j]) return false;
}
}
return true;
}
ll test(ll step) {
int cnt = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (s[i][j] != target[i][j])cnt++;
}
}
if (cnt + step > k)return 0;//当前与目标状态的不同位置数加上已经走了的步数小于最大搜索限制
else return 1;
}
// step:当前搜索深度 x,y:当前0的坐标 pre:上一次选择的策略
void Astar(ll step, ll x, ll y, ll pre) {
if (step == k) { if (check()) judge = 1; return; }//达到了限制搜索的最大深度
if (judge)return;
for (int i = 0; i < 4; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (xx < 0 || xx>2 || yy < 0 || yy>2 || pre + i == 3)continue;
swap(s[xx][yy], s[x][y]);
if (test(step) && !judge) Astar(step + 1, xx, yy, i);
swap(s[xx][yy], s[x][y]);
}
}
int main() {
ll x, y;
char ss[9];
cin >> ss;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
s[i][j] = ss[i * 3 + j] - '0';
if (s[i][j] == 0) x = i, y = j;
}
}
if (check()) { cout << 0 << endl; return 0; }//特判不需要移动的情况
while (++k) {
Astar(0, x, y, -1);
if (judge) { cout << k << endl; break; }
}
}