题目
Description
在一个 5 × 5 5 \times 5 5×5 的棋盘上有 12 12 12 个白色的骑士和 12 12 12 个黑色的骑士, 且有一个空位。在任何时候一个骑士都能按照骑 士的走法(它可以走到和它横坐标相差为 1 1 1 ,纵坐标相差为 2 2 2 或者横坐标相差为 2 2 2 ,纵坐标相差为 1 1 1 的格子)移动到空位上。 给定一个初始的棋盘,怎样才能经过移动变成如下目标棋盘: 为了体现出骑士精神,他们必须以最少的步数完成任务。
Input
第一行有一个正整数 T T T ( T < = 10 ) (T<=10) (T<=10),表示一共有 N N N 组数据。接下来有 T T T 个 5 × 5 5 \times 5 5×5 的矩阵, 0 0 0 表示白色骑士, 1 1 1 表示黑色骑士, ∗ * ∗ 表示空位。两组数据之间没有空行。
Output
对于每组数据都输出一行。如果能在 15 15 15 步以内(包括 15 15 15 步)到达目标状态,则输出步数,否则输出 − 1 -1 −1。
Sample Input
2
10110
01*11
10111
01001
00000
01011
110*1
01110
01010
00100
Sample Output
7
-1
思路
- 首先,最多 15 15 15步所以肯定是暴搜。
- 纯爆搜肯定是超时的
(你觉得一道省选题的出题人能这么良心?)。- 首先,这题肯定不能按马走,分支太多了。
(虽然我一开始并没有想到)- 于是我们要考虑让空格走。
- 可接着再怎么优化呢?
- 我们这里有两种办法。
- 1、同时限制了步数和终态,首先我们能想到双向bfs。
- 2、如果我们想用dfs的话,就要借助IDA*的帮助。
1、双向bfs
双向bfs,一个从头搜,一个从尾搜,同时进行,可以大大减少时间复杂度。
看到起始状态,目标状态已经给出,我们就能轻松想到用双向bfs。
主要难点就在于状态的记录。
如何记录一个棋盘的状态呢?
——Hash
首先我们先把这个矩阵转换成数字矩阵,
0
0
0 和
1
1
1 不变,
∗
*
∗ 变成
2
2
2。
然后再想办法把数字矩阵存下来。
那么如何把这个矩阵存下来呢? (我也不会呀)
- 把这个矩阵转成一个 25 25 25位数。
- 数字未免有点太大了。
- 难道必须要用字符串了吗?
(我觉得可以)- 于是我们想到状态压缩( 3 3 3进制)。
我们发现,矩阵里的数字无非就
3
3
3 个:
0
,
1
,
2
0,1,2
0,1,2。
很容易想到用三进制存储。
long long getHash(int (*x)[20]) {
long long t = 1, ret = 0;
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
ret += x[i][j] * (t *= 3);
return ret;
}
最大值也就 3 25 3^{25} 325
其中,
3
25
=
847288609443
3^{25}=847288609443
325=847288609443 (这个东西并没有爆long long)。
我们开一个这麽大的数组来存是绝对不现实的。
于是,map悄悄地走了过来,轻而易举的完成了这项任务。
用两个
m
a
p
map
map,一个记录从头开始的状态,另一个记录从尾开始的状态。
快活啊 😉。
2、IDA*搜索
- I D A ∗ IDA* IDA∗ 算法就是基于迭代加深的A*算法。
- 由于改成了深度优先的方式,与 A ∗ A* A∗ 比起来, I D A ∗ IDA* IDA∗ 更实用:
- 不需要判重,不需要排序。
- 空间需求减少。
IDDFS
IDDFS就是迭代加深搜索。
既然这个算法的名字叫做迭代加深搜索,那么它的核心在于“迭代加深”了。
那么什么是迭代加深呢?
我们知道,dfs有一种剪枝,当当前局面已经超出了我们所已经搜过的较优解时,当前局面一定不是最优解,想都不用想,直接 return。
有的时候,我们搜索已经搜了很深了,仍找不到一个较优解,那就很慢了。
(没事,能骗到分就行。)
我们可以给搜索设置一个约束,当深度已经达到约束却还没有一个答案的时候就 return。
如果当前约束并无法找到答案,我们就将约束深度调整的更深一点,再次搜索,直至搜到答案。
A*
A*算法,又称启发式搜索
A*算法比较核心的地方,就在于估价函数了。
定义:
f
(
n
)
=
g
(
n
)
+
h
(
n
)
f(n)=g(n)+h(n)
f(n)=g(n)+h(n)
其中
f
(
n
)
f(n)
f(n)是节点的估价函数,
g
(
n
)
g(n)
g(n)为从实际代价,
h
(
n
)
h(n)
h(n)是对往后状态的最完美估价。
想过这道题,我们就需要把这两种搜索结合在一起,用IDA*解决此题。
我们现在需要做的就是想一个比较优秀的估价函数。
注意:若估价函数不好的话,优化效率就很低,和爆搜没什么区别
(其实能得到分就不错)
我们每次将空白格子和棋子交换,最优的情况就是每次都把棋子移动到目标格子。
那么估价函数就出来了:
int goal[6][6] = {
{0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1},
{0, 0, 1, 1, 1, 1},
{0, 0, 0, 2, 1, 1},
{0, 0, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0},
};
int calc() {
int ret = 0;
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
if (a[i][j] != goal[i][j]) ret++;
return ret;
}
然后在搜索时加上一句:
if (now-1+calc() > sum) return;//最优性剪枝
最后再配上迭代加深搜索:
for (int i = 1; i <= 15; i++) {
flg = 0, ans = 1292371547;//请暂时忽略flg
A_star_search(0, x, y, -1);
if (ans == i) break;
}
再次快活 😃
代码
双向bfs
//1
#include <bits/stdc++.h>
using namespace std;
struct node {
int a[20][20], x, y;
bool st_ed;
} fir, goal;
const int dx[10] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int dy[10] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
struct nfm{
bool vis;
int step;
}; map <long long, nfm> b[2];
long long getHash(int (*x)[20]) {
long long t = 1, ret = 0;
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
ret += x[i][j] * (t *= 3);
return ret;
}
int bfs() {
queue <node> q;
for (int i = 0; i < 2; i++) b[i].clear();
q.push(fir); q.push(goal);
b[0][getHash(fir.a)].vis = 1;
b[1][getHash(goal.a)].vis = 1;
while (!q.empty()) {
node f = q.front(); q.pop();
long long tmp = getHash(f.a);
int lx = f.x, ly = f.y;
if (b[!f.st_ed][tmp].vis == 1) return b[0][tmp].step+b[1][tmp].step;
for (int i = 1; i <= 8; i++) {
int nx = lx+dx[i], ny = ly+dy[i];
if (nx >= 1 && nx <= 5 && ny >= 1 && ny <= 5) {
swap(f.a[nx][ny], f.a[lx][ly]);
f.x = nx, f.y = ny;
long long tmp1 = getHash(f.a);
if (b[f.st_ed][tmp1].vis == 0) {
b[f.st_ed][tmp1].vis = 1;
b[f.st_ed][tmp1].step = b[f.st_ed][tmp].step+1;
if (b[f.st_ed][tmp1].step < 8) q.push(f);
}
swap(f.a[nx][ny], f.a[lx][ly]);
f.x = lx, f.y = ly;
}
}
}
return -1;
}
int main() {
int T; scanf("%d", &T);
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= i-1; j++) goal.a[i][j] = 0;
for (int j = i; j <= 5; j++) goal.a[i][j] = 1;
}
for (int i = 4; i <= 5; i++) {
for (int j = 1; j <= i; j++) goal.a[i][j] = 0;
for (int j = i+1; j <= 5; j++) goal.a[i][j] = 1;
}
goal.a[3][3] = 2; goal.x = goal.y = 3; goal.st_ed = 1;
while (T--) {
for (int i = 1; i <= 5; i++) {
char c[110];
scanf("%s", c);
for (int j = 0; j < 5; j++)
if (c[j] == '*') fir.x = i, fir.y = j+1, fir.a[i][j+1] = 2;
else if (c[j] == '1') fir.a[i][j+1] = 1;
else fir.a[i][j+1] = 0;
}
fir.st_ed = 0;
int ans = bfs();
if (ans > 15) puts("-1");
else printf ("%d\n", ans);
}
return 0;
}
IDA*搜索
//2
#include <bits/stdc++.h>
using namespace std;
int goal[6][6] = {
{0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 1, 1},
{0, 0, 1, 1, 1, 1},
{0, 0, 0, 2, 1, 1},
{0, 0, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0},
};
const int dx[10] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int dy[10] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
int sx, sy, a[10][10], ans;
bool flg;
int calc() {
int ret = 0;
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
if (a[i][j] != goal[i][j]) ret++;
return ret;
}
bool okJump(int x, int y) {
return x >= 1 && x <= 5 && y >= 1 && y <= 5;
}
void A_star_search(int now, int x, int y, int k) {
if (flg) return ;
if (now == k) {
if (calc() == 0) flg = 1, ans = k;
return ;
}
if (now-1+calc() > k) return ;
for (int i = 1; i <= 8; i++) {
int nx = x+dx[i], ny = y+dy[i];
if (okJump(nx, ny)) {
swap(a[x][y], a[nx][ny]);
A_star_search(now+1, nx, ny, k);
swap(a[x][y], a[nx][ny]);
}
}
return ;
}
int main() {
int T; scanf("%d", &T);
while (T--) {
int sx, sy;
for (int i = 1; i <= 5; i++) {
char c[110];
scanf("%s", c);
for (int j = 0; j < 5; j++)
if (c[j] == '*') a[i][j+1] = 2, sx = i, sy = j+1;
else if (c[j] == '1') a[i][j+1] = 1;
else a[i][j+1] = 0;
}
for (int i = 0; i <= 15; i++) {
flg = 0; ans = 1292371547;
A_star_search(0, sx, sy, i);
if (ans == i) break;
}
if (ans <= 15) printf("%d\n", ans);
else puts("-1");
}
return 0;
}