The goal of the 15 puzzle problem is to complete pieces on 4×44×4 cells where one of the cells is empty space.
In this problem, the space is represented by 0 and pieces are represented by integers from 1 to 15 as shown below.
1 2 3 4 6 7 8 0 5 10 11 12 9 13 14 15
You can move a piece toward the empty space at one step. Your goal is to make the pieces the following configuration in the shortest move (fewest steps).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0
Write a program which reads an initial state of the puzzle and prints the fewest steps to solve the puzzle.
Input
The 4×44×4 integers denoting the pieces or space are given.
Output
Print the fewest steps in a line.
Constraints
- The given puzzle is solvable in at most 45 steps.
Sample Input
1 2 3 4 6 7 8 0 5 10 11 12 9 13 14 15
Sample Output
8
题意:给你一行16个数,四行四列,让你看看经过多少步变为 题意上给出的固定的 目标状态;
思路:刚开始 刚开始我没有学 IDA*时,我觉着 用 bfs()用康拓展开式,或用 map标记状态;但是想想 一定超时,因为有16! 种状态,把很多必要的状态都 存上了,不是超时,就是 内存超限;
下面介绍一下: IDA* 迭代加深搜索(ID-DFS),其实先估算一个 从起始状态 到 目标状态的 最小距离 limit ,一般会有函数
当前状态v下 到 目标状态 的估算函数 d(v) + h*(v) <=limit , 一般情况下, d(v) 为从起始状态到达当前状态所递归调用的深度; h*(v) 表示从当前v状态到 目标状态的 最小距离(估算值),limit 为当前 从起始状态到目标状态的 最小距离(估算值),要是找不到的话,一直枚举limit,让 limit 加 1,再从头再找,直到 达到目标状态,第一次找到 目标状态就是最优解,因为我们是枚举的 limit ,一次找到当然是最优解;
在这道题中可能有人会问 limit每次加1,都要从头再搜,这难道不更耗时时吗,还有就是在代码中,根本就都没有判重状态, 就是因为这道题 判重不易,所以选择了 IDA*(迭代加深搜索),当limit小的时候,因为limit的限制,所以能搜到的状态数不会太多,还有这道题 会有 16! 种状态,你要是用 map 判重,log N 的复杂度,16! 为一个 以2开头的十四位数字,是 2的40次方大的数,要查询大约 40次左右,而 IDA* 就算是不判重,是因为我们考虑搜索树中的一个节点,如果它(假设我们做一个简单的剪枝、拒绝反着走到达当前状态的最后一步)被再次搜到,就意味着它至少又转了四下。也就是说一个点至多被搜504≈13
,即其常数约为13!也就是说这其实是比用平衡树判重快得多的。但是ID-A*是不能像A*一样每次取出预估最优的状态的,它只能通过调整上下来解决这个问题;这跟DFS与ID-DFS的区别还不一样,因为在一个预估较劣的状态可能会达到一个预估较优的状态。
下面 介绍如何判断给出的起始 N数码能否到达到给出的目标状态;
先介绍一下逆序数:在一列数中,每个数前面有几个比自己大的数,就是这个数的逆序数,这一列数的逆序数就是这一列数的逆序数的总和;
先给出结论 :
在算N数码的逆序数时,不把0算入在内;
当N为奇数时, 当 两个N数码的逆序数 奇偶性相同时,可以互达,否则不行;
当N为偶数时,当 两个N数码的奇偶性相同的话,那么两个N数码中的0所在行的差值 k,k也必须是偶数时,才能互达;
当两个N数码的奇偶性不同时,那么两个N数码中的0所在行的差值 k,k也必须是奇数时,才能互达;
为什么呢:因为当0左右移动时,这个N数码的逆序数是不变的, 当上下移动时,当N为奇数时,上下移动时,中间有N-1个数,N-1 为偶数,那么整个N数码的逆序数只会有两种可能 加减一个偶数;举例说明:(当N为3时,N-1为2,当0上下移动时,中间的两个数的逆序数 有三种可能,同时加 1 或同时减1 ,或 一个加1,一个减1,这三种情况都使得总体的逆序数 增加或减少偶数个,所以不管 上下移几次,总体的逆序数的奇偶性是不变得)。当N为偶数时,上下移动时,中间有N-1个数,N-1为奇数,上下移动一次整个N数码的逆序数只会有 加上或减去一个奇数;举例说明:(当N为4时,N-1为3,当0上下移动时,中间的3个数的逆序数有四种情况,0个增加1、3个减少1,1个增加1、2个减少1,2个增加1,1个减少1,3个增加1、0个减少1,这四种情况全部都是 使全部的逆序数增加或减去 一个奇数),有了这个分析上的结论就好理解了
在写这个代码时,还有注意3点:找逆序数时不能算0的 逆序数; 找曼哈顿距离时,不能算0的曼哈顿距离; 在搜索时,一定要注意不能回搜;
代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#include<stdlib.h>
#define ll long long
const int Max = 4;
int a[4][2] = {0,-1,1,0,0,1,-1,0};
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 mpp[Max][Max],mpp2[Max*Max];
int flag = 0,limit,Mi;
// 曼哈顿距离为 所有的数字要走到目标状态,最少要和0换的次数;
int ddd(int mpp[Max][Max])
{
int i,j;
int sum = 0;
for(i = 0;i<4;i++)
{
for(j = 0;j<4;j++)
{
if(mpp[i][j]!=0) //判断曼哈顿距离不能判断0;
sum += abs(i-goal[mpp[i][j]][0])+abs(j - goal[mpp[i][j]][1]);
}
}
return sum;
}
int nix(int mpp2[Max*Max])
{
int i,j;
int x,y;
int sum = 0;
for(i =0;i<Max*Max;i++)
{
if(mpp2[i]==0)
{
y = i/4;
x = i%4;
continue;
}
for(j = i+1;j<Max*Max;j++)
{
if(mpp2[j]==0) continue;
if(mpp2[i]>mpp2[j])
sum++;
}
}
return sum;
}
void dfs(int y,int x,int len,int f) // x,y当前0的坐标 len 为已经走了几步了;
{ // f为当上一次的搜索方向 为了不回搜,
int d = ddd(mpp);
if(flag) return;
if(len<=limit)
{
if(d==0)
{
flag = 1;
Mi = len;
return ;
}
if(len==limit) return;
}
for(int i = 0;i<4;i++)
{
int tx = x + a[i][0];
int ty = y + a[i][1];
if(tx>=0&&ty>=0&&ty<4&&tx<4&&((f==-1)||i!=(f+2)%4))
{
swap(mpp[y][x],mpp[ty][tx]);
if(len+ddd(mpp)<=limit) // IDA* 减值,当前走的步数 加上 当前状态到达标状态的最小步数,
{ // 要小于等于 当前枚举到的最小的 从起始状态到达目标状态的步数,不能超过;
dfs(ty,tx,len+1,i);
if(flag) return ;
}
swap(mpp[y][x],mpp[ty][tx]);
}
}
}
int main()
{
int i,j;
while(~scanf("%d",&mpp2[0]))
{
int y,x;
mpp[0][0] = mpp2[0];
if(mpp2[0]==0)
{
y = 0;
x = 0;
}
for(i = 1;i<Max*Max;i++)
{
scanf("%d",&mpp2[i]);
mpp[i/4][i%4] = mpp2[i];
if(mpp2[i]==0)
{
y = i/4;
x = i%4;
}
}
int k = abs(y-goal[0][0]);//+abs(x-goal[0][0]);
int pp = nix(mpp2);
int f = 1;
if((pp+k)%2!=0)
f = 0;
if(!f)
{
//printf("pp==%d\n",pp);
//printf("00000000000\n");
continue; //因为题意说过了,不会有不会到达的这种情况,但是我还是判断了
}
flag = 0;
limit = ddd(mpp); // 当前要达到目标状态的最小步数;
while(!flag&&limit<=55)
{
dfs(y,x,0,-1);
if(!flag)
limit ++; // 枚举最小步数;
}
if(flag)
printf("%d\n",Mi);
}
return 0;
}