/这是我有史以来第一次不是先上代码的,因为我认为这个题目折射出的思想光辉比问题本身更重要/
今天在做完八数码难题后发现其实一道经典省选题能扩展出许多新思想或者算法,比如众所周知的八数码问题,大家一定有不同的解法,有用双向广搜的,有用单向+康托展开的,有用A*算法的,在此我使用的是康托展开+单向bfs。
其实打单向bfs也能过一些数据,但一定会超时,毕竟肯定会有重复的状态你重复了搜索,因此判重在此变成一个很重要的问题。那么如何进行判重呢?在此介绍康托展开算法。其实在百度上也能搜得到,但是百度百科毕竟不好理解,分享一下我对康托展开的理解
1、要注意康托展开是从0开始计算的,因此我们所排序的第1,2,3名在康托里需变成第0,1,2名(这很重要,如果你之后又折回看这条,说明你没认真看!)。
2、举个例子给大家:213的映射值X(也就是康托展开后计算的数值是多少),在此我们不妨将1,2,3的全排列写出来(记住一定要按字典序)
123 132 213 231 312 321
因此213排第二位(注意1的提示),那么它是怎么得到的呢?按公式推演如下:
1*2!+0*1!+0*0!=2
首先1、0、0是表示有多少数比当前数小,比如:比2小的只有1,因此前一位乘的是1,此时2计算过了,踢除;比1小的没有,因此前一位乘的是0,踢除;这里是重点,比3小的有1,2,但是之前1,2算过要被踢除了,因此前一位乘的也是0。好了,前面乘数解释完毕。那后面是意味着什么呢?读者先自己思考一下
—————————————–手动分割线,滑稽————————————————–
—————————————–手动分割线,滑稽————————————————–
不知道同学有没有注意到,2!代表2后面有2个数能填,1!代表1后面有1个数能填,0!代表3后面有0个数可填(无数可填),所以这就是这些阶乘的含义了。
好了,读者看到这里,要小试牛刀一下了
4123的映射值X是多少呢?(先自己思考,不要看答案,不准自己手动写字典序)
—————————————–手动分割线,滑稽————————————————–
3*3!+0*2!+0(1已经填过被排除了)*1!+0*0!=18(不要忘记提示1!!!)
好了,判重讲完了,现在介绍一下bfs的基本思路,首先我们假定只有空格为9且只有空格能动(其余是跟着空格动的),好,每次移动完后计算一下此时的康托展开式的值X是多少,如果达到目标,则马上输出步数然后终止程序,如果此时计算的值与之前有重复,那么则将这种新状态舍去(因此我们需要一个bool数组来存每个值是否被使用过),这样是不是会大大地减短超时的时间呢?
好了,话不多说,上完题目后上代码,字里行间自有注释
题目描述
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
输入输出格式
输入格式:
输入初试状态,一行九个数字,空格用0表示
输出格式:
只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数(测试数据中无特殊无法到达目标状态数据)
输入输出样例
输入样例#1:
283104765
输出样例#1:
4
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int js[10]={1,1,2,6,24,120,720,5040,40320};
int step[500000][2];//第二维中0用来存到达这个状态的步数,1来存空格的位置
int now[500000][10];//用来存整个棋盘状态
bool map[500000];//用来判断这个状态到达过没有
inline int read()
{
char c=getchar();
int l=0;
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')
{
now[1][l]=c-'0';
if(c=='0')
{
step[1][1]=l;
}
l++;
c=getchar();
}
step[1][0]=0;
}//完美的读入就此完成
int cantor(int a[10])
{
int sum=0;
for(int i=0;i<=8;i++)
{
int k=0;
for(int j=i+1;j<=8;j++)
{
if(a[i]>a[j])
{
k++;
}//计算an,跟在它身后有多少位
}
sum+=k*js[8-i];
}
return sum;
}//完美的判重就此完成
inline bool pd(int pos,int handle)//pos表示此时空格位置,handle表示如何处理
{
switch(handle)
{
case 1://向上移
if(pos>=3)return 1;
else return 0;
case 2://向左移
if(pos!=0&&pos!=3&&pos!=6)return 1;
else return 0;
case 3://向右移
if(pos!=2&&pos!=5&&pos!=8)return 1;
else return 0;
case 4://向下移
if(pos<=5)return 1;
else return 0;
}
}
void bfs()
{
int head=0,tail=1;
while(head<tail)
{
head++;
for(int i=1;i<=4;i++)
{
if(pd(step[head][1],i)!=0)
{
tail++;
for(int j=0;j<=8;j++)
{
now[tail][j]=now[head][j];
}
step[tail][0]=step[head][0]+1;
step[tail][1]=step[head][1]+2*i-5;//自己模拟一下
swap(now[tail][step[head][1]],now[tail][step[tail][1]]);
int s=cantor(now[tail]);//把现在这一行(状态)传过去
if(s==46685)//目标状态,不信可以自己算
{
cout<<step[tail][0]<<endl;
return;
}
if(map[s]==1)tail--;//新目标与子节点重复,舍去
map[s]=1;//标记一下
}
}
}
}//完美的单向bfs就此完成
int main()
{
read();
bfs();
return 0;
}
这是我第一次写这么认真的博客,说实话想了很久也不知道放题解组还是知识组,但后面又重新查阅了一遍觉得还是放知识组吧,毕竟这个题目的算法太多,希望读者看完后也不要仅限于这一种方法,大家能一起多多讨论。
Thanks for your attention.