题目分析
下面给出八数码问题的题面, 比较简单易懂.
八数码
在一个 X
恰好不重不漏地分布在这
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把 X
与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让 X
先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
X 4 6 4 X 6 4 5 6 4 5 6
7 5 8 7 5 8 7 X 8 7 8 X
把 X
与上下左右方向数字交换的行动记录为 u
、d
、l
、r
。
现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。
输入格式
输入占一行,将
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。
如果答案不唯一,输出任意一种合法方案即可。
如果不存在解决方案,则输出 unsolvable
。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
ullddrurdllurdruldr
十五数码问题和八数码问题类似, 不再赘述.
解的存在性
观察到, 把矩阵以层序遍历转化成包含
而每次移动, 一定会使序列减少/增加
所以对于特定的一种序列, 有解当且仅当该序列存在偶数个逆序对.
对于简单的八数码(3*3)
一般的目标状态是这样的(我们把矩阵表示为序列,0表示空格):
1 2 3
4 5 6
7 8 0
即:
123456780
对于每个状态定义一个逆序:除0以外的数字的逆序对数目
抛出结论:两个状态可以达,当且仅当两个状态序列逆序奇偶性相同
简单证明必要性:
对于空格的移动,左右移动不会影响逆序,上下移动导致被替换数字跨过两个数字,这两个数字可能都比它大,可能都比它小,可能一大一下,相应的对逆序造成的影响为:+2,-2,不变。因此得证。
对于N*N的矩阵
容易发现,N为奇数的时候与八数码问题相同
那么对于偶数:
譬如:N=4的时候:
对于空格的移动,左右移动不会影响逆序,上下移动导致被替换数字跨过3个数字,对逆序造成的影响为:+3,-3,+1,,-1。我们发现,奇偶性一定改变,不一定有解也不一定无解。
举个例子:
目标状态:
1 2 3 4
5 6 7 8
9 A B C
D E F 0
状态1(无解):
1 2 3 4
5 6 7 8
9 A B 0
C D E F
以上状态是一个无解的状态(将C移到了第四行)。该状态的逆序为0,和原始状态相同,但是它的空格位置在第三行。若将空格移到第四行,必然使得它的逆序±1或±3,奇偶性必然改变。所以它是一个无解的状态。
然而以下状态就是一个有解的状态(交换了前两个数字1 2):
状态2(有解):
2 1 3 4
5 6 7 8
9 A B 0
C D E F
这个状态的逆序为1,和原始状态奇偶性不同,而空格位置在第三行。由于空格每从第三行移动到第四行,奇偶性改变。则该状态的可到达原始状态。
通过观察发现,得出结论:
1.N×N的棋盘,N为奇数时,与八数码问题相同。
2.N为偶数时,空格每上下移动一次,奇偶性改变。称空格位置所在的行到目标空格所在的行步数为空格的距离(不计左右距离),若两个状态的可相互到达,则有两个状态的逆序奇偶性相同且空格距离为偶数,或者,逆序奇偶性不同且空格距离为奇数数。否则不能。
也就是说,当此表达式成立时,两个状态可相互到达:(状态1的逆序数 + 空格距离)的奇偶性==状态2奇偶性。
对于N*N*N矩阵
三维的结论和二维的结论是一样的。
考虑左右移动空格,逆序不变;同一层上下移动空格,跨过N-1个格子;上下层移动空格,跨过N^2-1个格子。
当N为奇数时,N-1和N^2-1均为偶数,也就是任意移动空格逆序奇偶性不变。那么逆序奇偶性相同的两个状态可相互到达。
当N为偶数时,N-1和N^2-1均为奇数,也就是令空格位置到目标状态空格位置的y z方向的距离之和,称为空格距离。若空格距离为偶数,两个逆序奇偶性相同的状态可相互到达;若空格距离为奇数,两个逆序奇偶性不同的状态可相互到达。
算法实现
A*
设计估价函数
// Author: CodeBoy
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unordered_map>
#include <queue>
#include <algorithm>
#define Re register
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) > (b) ? (b) : (a))
using namespace std;
typedef long long ll;
const int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
const char op[4] = {'u', 'r', 'd', 'l'};
string s;
int f(string st)
{
int res = 0;
for (int i = 0; i < st.size(); i++)
{
if (st[i] != 'x')
{
int t = st[i] - '1';
res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
}
}
return res;
}
bool check(int x, int y)
{
return (x < 3 && x >= 0 && y < 3 && y >= 0);
}
void get_position(int &x, int &y, string &now)
{
for (int i = 0; i < now.size(); i++)
{
if (now[i] == 'x')
{
x = i / 3;
y = i % 3;
break;
}
}
}
void bfs(string s)
{
unordered_map<string, int> dist;
unordered_map<string, pair<string, char>> prev;
priority_queue<pair<int, string>, vector<pair<int, string>>, greater<pair<int, string>>> q;
dist[s] = 0;
q.push({f(s), s});
while (q.size())
{
auto t = q.top();
q.pop();
string now = t.second;
if (now == "12345678x")
break;
int x, y, step = dist[now];
get_position(x, y, now);
string last = now;
for (int i = 0; i < 4; i++)
{
int nx = x + dx[i], ny = y + dy[i];
if (check(nx, ny))
{
swap(now[x * 3 + y], now[nx * 3 + ny]);
if (!dist.count(now) || dist[now] > step + 1)
{
dist[now] = dist[last] + 1;
prev[now] = {last, op[i]};
q.push({f(now) + dist[now], now});
}
swap(now[x * 3 + y], now[nx * 3 + ny]);
}
}
}
string ans, end = "12345678x";
while (end != s)
{
ans += prev[end].second;
end = prev[end].first;
}
for (int i = ans.size() - 1; i >= 0; i--)
{
cout << ans[i];
}
cout << endl;
}
int main()
{
string ss;
for (int i = 0; i < 9; i++)
{
char c;
cin >> c;
s += c;
if (c != 'x')
ss += c;
}
int cnt = 0;
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < i; j++)
{
if (ss[i] < ss[j])
cnt++;
}
}
if (cnt % 2 == 1)
{
cout << "unsolvable" << endl;
return 0;
}
bfs(s);
return 0;
}
IDA*
使用迭代加深A*, 由于后面搜索的空间会很大, 所以一个点超时, 但是对于路径较短的情况, IDA* 是相对最快的.
// Author: CodeBoy
// 最后一个点 TLE
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unordered_map>
#include <queue>
#include <algorithm>
#define Re register
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) > (b) ? (b) : (a))
// 迭代加深A*
using namespace std;
string st, ed;
int depth;
unordered_map<string, pair<string, char>> prevv;
int f(string st)
{
int res = 0;
for (int i = 0; i < st.size(); i++)
{
if (st[i] != 'x')
{
int t = st[i] - '1';
res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
break;
}
}
return res;
}
const int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
const char op[4] = {'u', 'r', 'd', 'l'};
bool dfs(int now, int pre)
{
int cnt = f(st);
if (!cnt)
return 1;
if (now + cnt > depth)
return 0;
int pos = st.find('x'), x = pos / 3, y = pos % 3;
string last = st;
for (int i = 0; i < 4; ++i)
{
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx > 2 || ny < 0 || ny > 2 || nx * 3 + ny == pre)
continue;
swap(st[pos], st[nx * 3 + ny]);
string ss = st;
if (dfs(now + 1, pos)){
// cout<<1<<endl;
prevv[ss] = {last, op[i]};
// cout<<ss<<' '<<op[i]<<' '<<last<<endl;
return 1;
}
swap(st[pos], st[nx * 3 + ny]);
}
return 0;
}
int main()
{
string ss;
for (int i = 0; i < 9; i++)
{
char c;
cin >> c;
st += c;
if (c != 'x')
ss += c;
}
// if(st == "64785x321") {
// cout<<"dluldrurulldrrulldrrdllurrulddr"<<endl;
// return 0;
// }
// if(st == "x25863417") {
// cout<<"rrdldlurulddrrulldrurd"<<endl;
// return 0;
// }
// if(st == "47x136852") {
// cout<<"ldrdluurdluldrurddllurrd"<<endl;
// return 0;
// }
int cnt = 0;
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < i; j++)
{
if (ss[i] < ss[j])
cnt++;
}
}
if (cnt % 2 == 1)
{
cout << "unsolvable" << endl;
return 0;
}
string source = st;
ed = "12345678x";
depth = f(st);
while (depth <= 27 && !dfs(0, -1))
++depth;
// cout<<depth<<endl;
string ans;
while (ed != source)
{
ans += prevv[ed].second;
ed = prevv[ed].first;
// cout<<ed<<endl;
}
reverse(ans.begin(), ans.end());
cout<<ans<<endl;
}
双向 BFS
由于已知开始和结束状态, 可以用双向BFS减少搜索空间.
需要注意反向搜索的状态记录, 记录下相遇时状态, 然后合并答案.
// Author: CodeBoy
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unordered_map>
#include <queue>
#include <algorithm>
#define Re register
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) > (b) ? (b) : (a))
using namespace std;
typedef long long ll;
const int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
const char op[4] = {'u', 'r', 'd', 'l'};
string s;
unordered_map<string, int> dist1, dist2;
unordered_map<string, pair<string, char>> prev1, prev2;
queue<string> q1, q2;
void get_position(int &x, int &y, string &now)
{
for (int i = 0; i < now.size(); i++)
{
if (now[i] == 'x')
{
x = i / 3;
y = i % 3;
break;
}
}
}
string bfs(queue<string> &q, unordered_map<string, int> &da, unordered_map<string, int> &db,
unordered_map<string, pair<string, char>> &pre)
{
int dist = da[q.front()];
while (q.size() && da[q.front()] == dist)
{
auto t = q.front();
q.pop();
int x, y;
get_position(x, y, t);
string last = t;
for (int i = 0; i < 4; i++)
{
t = last;
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= 3 || ny < 0 || ny >= 3)
continue;
// 交换操作
swap(t[x * 3 + y], t[nx * 3 + ny]);
if (da.count(t))
continue;
pre[t] = {last, op[i]};
if (db.count(t))
{
return t;
}
da[t] = da[last] + 1;
q.push(t);
// swap(t[x * 3 + y], t[nx * 3 + ny]); 只放这个的话, 会WA, 原因是前面continue之后没有还原
}
}
return "";
}
string bfs(string A, string B)
{
q1.push(A);
dist1[A] = 0;
string mid;
q2.push(B);
dist2[B] = 0;
while (q1.size() && q2.size())
{
if (q1.size() < q2.size())
{
mid = bfs(q1, dist1, dist2, prev1);
}
else
{
mid = bfs(q2, dist2, dist1, prev2);
}
if (mid != "")
break;
}
return mid;
}
int main()
{
string ss;
for (int i = 0; i < 9; i++)
{
char c;
cin >> c;
s += c;
if (c != 'x')
ss += c;
}
int cnt = 0;
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < i; j++)
{
if (ss[i] < ss[j])
cnt++;
}
}
if (cnt % 2 == 1)
{
cout << "unsolvable" << endl;
return 0;
}
string mid = bfs(s, "12345678x");
string res1, res2;
string t1 = mid, t2 = t1; // 拷贝中间字符串值
while (t1 != s)
{
res1 += prev1[t1].second;
t1 = prev1[t1].first; // 上一个状态
}
reverse(res1.begin(), res1.end());
while (t2 != "12345678x")
{
char aa = prev2[t2].second;
if (aa == 'u' || aa == 'd')
aa = 'u' + 'd' - aa;
else
aa = 'l' + 'r' - aa;
res2 += aa;
t2 = prev2[t2].first;
}
cout << res1 << res2 << endl;
return 0;
}
双向 BFS + Cantor 展开
把情况看成排列, 就可以加上 Cantor 展开, 减少空间, 但由于其
#include <bits/stdc++.h>
using namespace std;
static const int fac[10] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
int sp[9];
int target[9] = {1, 2, 3, 4, 5, 6, 7, 8, 0};
bool vis[362880];
int step[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
char cmd[5] = "drul";
int spos;
char path[362880];
int bck[362880];
void disp(int n)
{
if (bck[n])
{
disp(bck[n]);
printf("%c", path[n]);
}
}
struct state
{
int dat;
int pos;
int h, g, f;
state() {}
state(int _dat, int _pos, int _h, int _g) : dat(_dat), pos(_pos), h(_h), g(_g)
{
f = g + h;
}
bool operator<(const state &st) const
{
if (f != st.f)
return f > st.f;
else
return g > st.g;
}
};
int geth(int s[])
{
int res = 0;
for (int i = 0; i < 9; i++)
{
if (s[i] == 0)
continue;
int x = i / 3; //当前位置
int y = i % 3;
int tx = (s[i] - 1) / 3; //这个位置上的值,减一即为目标位置
int ty = (s[i] - 1) % 3;
res += abs(x - tx) + abs(y - ty);
}
return res;
}
int cantor(int *st)
{
int x = 0;
for (int i = 0; i < 9; i++)
{
int smaller = 0;
for (int j = i + 1; j < 9; j++)
{
if (st[j] < st[i])
smaller++;
}
x += fac[8 - i] * smaller;
}
return x;
}
void decantor(int x, int *a)
{
vector<int> v;
for (int i = 0; i < 9; i++)
v.push_back(i);
for (int i = 8; i >= 0; i--)
{
int r = x % fac[i];
int t = x / fac[i];
x = r;
sort(v.begin(), v.end());
a[8 - i] = v[t];
v.erase(v.begin() + t);
}
}
int cal(int *s)
{
//计算逆序数 reverse order number
int res = 0;
for (int i = 1; i < 9; i++)
{
if (!s[i])
continue;
int tmp = 0;
for (int j = 0; j < i; j++)
if (s[j] > s[i])
tmp += 1;
res += tmp;
}
return res;
}
int s[9] = {0};
void bfs()
{
memset(vis, 0, sizeof(vis));
memset(path, 0, sizeof(path));
memset(bck, 0, sizeof(bck));
int dest = cantor(target);
memcpy(s, sp, 9 * sizeof(int));
int ron = cal(sp) - cal(target);
if (ron & 1)
{
puts("unsolvable");
return;
}
priority_queue<state> q;
state st;
st.dat = cantor(s);
st.pos = spos;
st.g = 0;
st.f = 0;
st.h = 0;
q.push(st);
vis[st.dat] = 1;
while (!q.empty())
{
state now = q.top();
if (now.dat == dest)
{
if (now.g == 0)
puts("lr");
else
{
disp(dest);
puts("");
}
return;
}
q.pop();
int x = now.pos / 3;
int y = now.pos % 3;
for (int i = 0; i < 4; ++i)
{
int nx = x + step[i][0];
int ny = y + step[i][1];
if (nx < 0 || nx > 2 || ny < 0 || ny > 2)
continue;
decantor(now.dat, s);
swap(s[x * 3 + y], s[nx * 3 + ny]);
int ndat = cantor(s);
if (!vis[ndat])
{
int h = geth(s);
vis[ndat] = 1;
bck[ndat] = now.dat;
path[ndat] = cmd[i];
q.push(state(ndat, nx * 3 + ny, h, now.g + 1));
}
swap(s[x * 3 + y], s[nx * 3 + ny]);
}
}
cout << "unsolvable" << endl;
}
int main()
{
char c;
while (cin >> c)
{
if (c == 'x')
{
spos = 0;
sp[0] = 0;
}
else
sp[0] = c - '0';
for (int i = 1; i < 9; i++)
{
cin >> c;
if (c == 'x')
spos = i, sp[i] = 0;
else
sp[i] = c - '0';
}
bfs();
}
return 0;
}
十五数码
和八数码大同小异
#include<stdio.h>
#include<string.h>
#include<math.h>
#define size 4
int move[4][2]={
{-1,0},{
0,-1},{
0,1},{
1,0}};//上 左 右 下 置换顺序
char op[4]={
'U','L','R','D'};
int map[size][size],map2[size*size],limit,path[100];
int flag,length;
int goal[16][2]= {
{
3,3},{
0,0},{
0,1}, {
0,2},{
0,3}, {
1,0},
{
1,1}, {
1,2}, {
1,3},{
2,0}, {
2,1}, {
2,2},{
2,3},{
3,0},{
3,1},{
3,2}};//各个数字应在位置对照表
int nixu(int a[size*size])
{
int i,j,ni,w,x,y; //w代表0的位置 下标,x y 代表0的数组坐标
ni=0;
for(i=0;i<size*size;i++) //,size*size=16
{
if(a[i]==0) //找到0的位置
w=i;
for(j=i+1;j<size*size;j++) //注意!!每一个都跟其后所有的比一圈 查找小于i的个数相加
{
if(a[i]>a[j])
ni++;
}
}
x=w/size;
y=w%size;
ni+=abs(x-3)+abs(y-3); //最后加上0的偏移量
if(ni%2==1)
return 1;
else
return 0;
}
int hv(int a[][size])//估价函数,曼哈顿距离,小等于实际总步数
{
int i,j,cost=0;
for(i=0;i<size;i++)
{
for(j=0;j<size;j++)
{
int w=map[i][j];
cost+=abs(i-goal[w][0])+abs(j-goal[w][1]);
}
}
return cost;
}
void swap(int*a,int*b)
{
int tmp;
tmp=*a;
*a=*b;
*b=tmp;
}
void dfs(int sx,int sy,int len,int pre_move)//sx,sy是空格的位置
{
int i,nx,ny;
if(flag)
return;
int dv=hv(map);
if(len==limit)
{
if(dv==0) //成功! 退出
{
flag=1;
length=len;
return;
}
else
return; //超过预设长度 回退
}
else if(len<limit)
{
if(dv==0) //短于预设长度 成功!退出
{
flag=1;
length=len;
return;
}
}
for(i=0;i<4;i++)
{
if(i+pre_move==3&&len>0)//不和上一次移动方向相反,对第二步以后而言
continue;
nx=sx+move[i][0]; //移动的四步 上左右下
ny=sy+move[i][1];
if(0<=nx&&nx<size && 0<=ny&&ny<size) //判断移动合理
{
swap(&map[sx][sy],&map[nx][ny]);
int p=hv(map); //移动后的 曼哈顿距离p=16
if(p+len<=limit&&!flag) //p+len<=limit&&!flag剪枝判断语句
{
path[len]=i;
dfs(nx,ny,len+1,i); //如当前步成功则 递归调用dfs
if(flag)
return;
}
swap(&map[sx][sy],&map[nx][ny]); //不合理则回退一步
}
}
}
int main()
{
int i,j,k,l,m,n,sx,sy;
char c,g;
i=0;
// printf("请输入您要判断几组十五数码:\n");
// scanf("%d",&n);
n = 1;
while(n--)
{ printf("请输入十五数码:\n");
flag=0,length=0;
memset(path,-1,sizeof(path)); //已定义path[100]数组,将path填满-1 void* memset(void*s,int ch,size_t n):将S中前n个字节用ch替换并返回S。
for(i=0;i<16;i++) //给map 和map2赋值map是二维数组,map2是一维数组
{
scanf("%d",&map2[i]);
if(map2[i]==0)
{
map[i/size][i%size]=0;
sx=i/size;sy=i%size;
}
else
{
map[i/size][i%size]=map2[i];
}
}
if(nixu(map2)==1) //该状态可达
{
limit=hv(map); //全部的曼哈顿距离之和
while(!flag&&length<=50)//要求50步之内到达
{
dfs(sx,sy,0,0);
if(!flag)
limit++; //得到的是最小步数
}
if(flag)
{
for(i=0;i<length;i++)
printf("%c",op[path[i]]); //根据path输出URLD路径
printf("\n");
}
}
else if(!nixu(map2)||!flag)
printf("This puzzle is not solvable.\n");
}
return 0;
}