八数码问题
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。
以上就是关于八数码问题的描述。
八数码与逆序对
现在给定两个把数码的游戏局面,请判断是否存在一种移动空格的方式,使得其中一个局面可以变化为另一个局面。
八数码游戏的两个局面可达,当且仅当把两个局面写成一维形式的时候,逆序对个数的奇偶性相同。证明很简单,分为两种情况。第一种情况:我们左右移动空格位置的时候,一维形式数列不变,所以逆序对奇偶性不变。第二种情况:我们上下移动空格位置的时候,在一维形式上的表现就是某个数与它前(后)面的 2 个数进行位置互换。此时我们又要分两种情况讨论。
- 与它进行位置交换的两个数都比它大(小)那么逆序对数量就减少(增加)2。逆序对数量奇偶性不变。
- 与他进行交换的两个数一个比他大,另一个比他小,那么逆序对的数量加一再减一就没有变化,所以逆序对数量奇偶性不变。
然后我们就可以对于两次给出的局面维护一个树状数组求出两次的逆序对数量(也可以用归并排序求逆序对)然后判断奇偶性是否相同就好了。
传送门:树状数组与逆序对
#include<bits/stdc++.h>
using namespace std;
int n = 8;
int a[10] = { 0 };
int b[10] = { 0 }; // 初状态
int c[10] = { 0 }; // 末状态
int t[50] = { 0 }; // 树状数组
inline int lowbit(int x){
return x & -x;
}
void add(int index, int val){
for(; index <= n; index += lowbit(index)){
t[index] += val;
}
}
int query(int x){
int ans = 0;
for(; x; x -= lowbit(x)){
ans += t[x];
}
return ans;
}
int main(){
scanf("%1d %1d %1d %1d %1d %1d %1d %1d %ld", &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7], &a[8], &a[9]);
bool is1 = false;
for(int i = 1; i <= n + 1; i++){
if(!a[i]) { is1 = true; continue; }
if(is1) b[i-1] = a[i];
else b[i] = a[i];
}
/*
for(int i = 1; i <= n; i++){
printf("%d ", b[i]);
}
puts("");
*/
scanf("%1d %1d %1d %1d %1d %1d %1d %1d %ld", &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7], &a[8], &a[9]);
bool is2 = false;
for(int i = 1; i <= n + 1; i++){
if(!a[i]) { is2 = true; continue; }
if(is2) c[i-1] = a[i];
else c[i] = a[i];
}
/*
for(int i = 1; i <= n; i++){
printf("%d ", c[i]);
}
puts("");
*/
int ans1 = 0;
for(int i = n; i >= 1; i--){
ans1 += query(b[i] - 1);
add(b[i], 1);
}
//printf("ans1 = %d\n", ans1);
memset(t, 0, sizeof(t));
int ans2 = 0;
for(int i = n; i >= 1; i--){
ans2 += query(c[i] - 1);
add(c[i], 1);
}
//printf("ans2 = %d\n", ans2);
if(ans1 % 2 == ans2 % 2){
printf("Can be reached\n");
}
else{
printf("Can not be reached\n");
}
return 0;
}
八数码问题与 A*
现在要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
首先先利用上述方法判断是否可解,若问题有解,那我们就采用 A* 算法搜索出一种移动部步数最小的方案。我们令评估函数为当前位置和目标位置有几个数的位置不相同,即:
f
(
s
t
a
t
e
)
=
∑
i
=
1
9
(
s
t
a
t
e
x
i
=
=
g
o
a
l
x
i
a
n
d
s
t
a
t
e
y
i
=
=
g
o
a
l
y
i
)
?
0
:
1
f(state) = \sum_{i=1}^{9} (state_{xi} == goal_{xi} \; and \; state_{yi} == goal_{yi}) \; ? \; 0 \; : \; 1
f(state)=i=1∑9(statexi==goalxiandstateyi==goalyi)?0:1
然后每次我们用当前步数 g(state) 加上 f(state) 来进行扩展,最终状态第一次被从堆中取出时就是最优解。
#include<bits/stdc++.h>
using namespace std;
const int px[4] = { 1, 0, -1, 0 };
const int py[4] = { 0, 1, 0, -1 };
int n = 8;
char s[20];
int x = 0; int y = 0; // 0 的坐标
string goal = "123804765";
int h(string current){ // 现在位置和目标位置有几个位置是不同的 (评估函数)
int ans = 0;
for(int i = 0; i < 9; i++){
if(goal[i] != current[i] and goal[i] != 0) ans++;
}
return ans;
}
struct Tnode{
int f, step; // 当前评估函数的值和走的步数
string now; // 当前排列
bool operator < (const Tnode &x) const {
return step + f > x.step + x.f; // 以当前步数加上评估函数排序
}
};
int ans = 0x7fffffff;
priority_queue<Tnode> q;
map<string, int> dis;
map<string, bool> way;
void bfs(char s[]){
way[s] = 1; dis[s] = 0;
q.push((Tnode){h(s), 0, s}); // 起点入堆
while(!q.empty()){
Tnode t = q.top(); q.pop();
string cur = t.now;
if(cur == "123804765"){
printf("%d\n", t.step);
exit(0);
}
int tx = 0; int ty = 0;
for(int i = 0; i < 9; i++){ // 当前排列 0 的位置
if(cur[i] - '0' == 0){
tx = i / 3 + 1; ty = i % 3 + 1;
}
}
int idx1 = (tx - 1) * 3 + ty - 1; // 在一维排列中的位置
for(int i = 0; i < 4; i++){ // 扩展
int nx = tx + px[i]; int ny = ty + py[i];
if(nx < 1 or nx > 3 or ny < 1 or ny > 3) continue;
int idx2 = (nx - 1) * 3 + ny - 1; // 扩展之后的位置
swap(cur[idx1], cur[idx2]);
if(!way[cur] or (way[cur] and (t.step + 1) < dis[cur])){
dis[cur] = t.step + 1;
q.push((Tnode){h(cur), t.step + 1, cur});
way[cur] = 1;
}
swap(cur[idx1], cur[idx2]);
}
}
}
int main(){
scanf("%s", s);
for(int i = 0; i < 9; i++){
if(s[i] - '0' == 0){
x = i / 3 + 1; y = i % 3 + 1; // 0 的初始坐标
}
}
if(h(s) == 0){ // 初始状态及目标状态 :直接返回
printf("%d\n", 0);
return 0;
}
while(!q.empty()) q.pop();
bfs(s); // 开始搜索
return 0;
}
以上为洛谷 P1379 的代码。传送门:luoguP1379