先放源码 大部分借鉴了书本的代码
随后对部分代码进行解释
#include
#include
#include
#define LEN 362880
using namespace std;
/*
* 八数码问题
*
* */
struct node{
int dp[9];
int dis;
};
int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
int vis[LEN]={0};
long int factory[]={1,1,2,6,24,120,720,5040,40320,362880,3628800};
int start[9];
int goal[9];
bool Cantor(int str[],int n){
long res=0;
for(int i=0;istr[j]){
++con;
}
}
res+=con*factory[n-i-1];//被选中的数后面还有几个数的全排列
}
if(!vis[res]){
vis[res]=1;
return 1;
}
else{return 0;}
}
int bfs(){
node head;
memcpy(head.dp,start, sizeof(start));
Cantor(head.dp,9);//对起点判重
head.dis=0;
queue queue;
queue.push(head);
while(!queue.empty()){
head=queue.front();
queue.pop();
int z;
for(z=0;z<9;z++)
if(head.dp[z]==0) break;
int L=z%3;
int H=z/3;
for(int i=0;i<4;i++){//方向上有四个方向
int newH=H+dir[i][1];
int newL=L+dir[i][0];
int newz=newH*3+newL;
if(newH>=0&&newH<3&&newL>=0&&newL<3){//有没有越界
node newdp;
memcpy(&newdp,&head, sizeof(struct node));
swap(newdp.dp[z],newdp.dp[newz]);
newdp.dis++;
if(memcmp(newdp.dp,goal,sizeof(goal))==0)
return newdp.dis;
if(Cantor(newdp.dp,9))
queue.push(newdp);
}
}
}
return -1;
}
int main() {
for(int i=0;i<9;i++) cin>> start[i];
for(int i=0;i<9;i++) cin>> goal[i];
int num=bfs();
if(num!=-1){cout<
在一个3×3的棋盘上放置编号为1到8的方块 每个占一格 另外还有一格是空格
与空格相邻的方块可以移动到空格上
任务1 :指定初始棋盘 和 目标棋盘 计算最少移动的步数
任务2 :输出移动的序列(本代码中并没有完成任务2)
本文代码中涉及到dp字样 但是很动态规划没有什么关系 只是用这个dp表示状态而已
首先剖析题意:
先把1到8的方块分别用1到8的阿拉伯数字表示
与空格方块相邻的方块可以移动到空格中(本文空格用阿拉伯数字0表示)
说明了就是0可以和相邻的方块交换位置
这点很重要
我们把这个棋盘的每种摆法称为一种状态
那么0可以在棋盘的第一位 (这是很显然的,也是理所因当的)
其次不难看出所有状态的个数 也就是 362880 个
对这个数字应该抱有敏感 就是9的阶乘 也就是9个数或方块的全排列
这个题目最大的问题就是 如果每次都把新产生的状态都与所有状态相对比 判断是不是目标状态
那么可能就会比上个362880×362880次的可能
当然这是基本上不可行的
我们假设初始状态是 123084765 目标状态是103824765
将他们转换成二维数组就是
1 2 3 1 0 3
0 8 4 8 2 4
7 6 5 7 6 5
不难看出初始到结束 最短的两步 0->8->2
我们需要对他遍历 目前0的位置有三种遍历的可能
4中遍历的方向都写在dir数组中
遍历的使用 通过for 和 if 遍历 和 选择 当且仅当方向在合法的情况下生成新的状态
因为是对0操作的 我们就需要开始找到0的位置
所以就有了z变量记录0的位置
而输入是一维数组
但是遍历方向的时候却是二维数组的形式 那么我们就需要把0的位置 就是变量z转化为二维的坐标
那么就定义H和L 表示二维的行 和 列
z/3是行
z%3是列 这个很好理解 不能理解的话就画画图把
我们不难发现 状态A(就是初始状态)向上遍历的时候 就到了角落
那么就只有两个方向可以遍历了
但是遍历的时候 向右和向下都是合法的方向
但是如果向下 不就等于回到了原点了吗
这么一个状态的比较 随随便便就回到了原来的状态并且还有继续产生新的状态不就是浪费资源么
那么我们就需要一个足够大的数组来表示这个状态已经处理过了
于是就有了数组
long int vis[362880]={0};
这个判重的方法我们就先不谈
先解决方向遍历如何产生新状态和如何处理旧状态的问题
新的状态进来 我们命名A 遍历了方向后 B C D进来 步数是1
同时A出去
B出去的同时 E F。。。进来
不断到目标状态
当然当一个状态进入的同时判断是不是处理过了
处理过了就直接出去
以上的描述 不难看出这个是先进先出表 就是队列
这个是处理状态的方法
接下来处理新产生的状态究竟有没有被处理过的方法
引入另一个方法
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
康托展开举例
再举个例子说明。
在 5个数的排列组合中,计算 34152的康托展开值。
首位是3,则小于3的数有两个,为1和2, ,则首位小于3的所有排列组合为
第二位是4,由于第一位小于4,1、2、3中一定会有1个充当第一位,所以排在4之下的只剩2个,所以其实计算的是在第二位之后小于4的个数。因此 。
第三位是1,则在其之后小于1的数有0个,所以 。
第四位是5,则在其之后小于5的数有1个,为2,所以 。
最后一位就不用计算啦,因为在它之后已经没有数了,所以 固定为0
根据公式:
所以比34152小的组合有61个,即34152是排第62。
此处借鉴百度百科
一般的线性表,树中,记录在结构中的相对位置是随机的,即和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较“的基础上,查找的效率依赖于查找过程中所进行的比较次数。 理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。
此处借鉴百度百科
康托展开是一种特殊的哈希函数
以下用cantor表示康托展开
在使用cantor的时候 他有什么功能 是我们要知道的 而且这个是思想 算法 需要我们认为去实现的
cantor可以在你给出的序列中计算这个序列在所有可能的排列里面它是第几个
那么利用这个用能我们在一个新的状态产生的时候 放入cantor函数中 判断他是第几个数 假设是第n个数
那么我们就让
vis[n]=1
下次还有新的状态产生的时候 恰好是第n个数 程序就知道这个状态已经遇见过了 没有处理的意义了 就放弃这个状态去产生另一个新的状态
假设数2143
判断他的cantor值 那么
1.首位比2小的 只有1 其余的3个数的全排列就是 1×2×3=6
2.首位是2 的时候 因为要计算这个数是第几大 只需要计算在这个数前面的有几个就可以了
首位是2 的时候 第二位比1 小的 没有 所以是0
3.前面两位是21的时候 后面两位比4小的只有3 那么就是1
4。前面三位是214的时候 最后以为是3 那么没有比3 小的数了 就是 0
所以就是6+0+1+0=7
所以cantor值是7
因为我们会把vis[7]=1;所以表示cantor是从0开始计算的
不信的话可以自己去写写
运用这个思想 就可以写出cantor函数了
我截取部分的函数来说明一下
bool Cantor(int str[],int n){
long res=0;
for(int i=0;istr[j]){
++con;
}
}
res+=con*factory[n-i-1];//被选中的数后面还有几个数的全排列
}
if(!vis[res]){
vis[res]=1;
return 1;
}
else{return 0;}
}
res计算这个是第几大的数
con计算当数组指向某个下位置的时候 比这个位置上的数在后面还要小的有几个
我们用它来计算全排列 1到10的全排列都放在了factory数组内
然后if判断是不是处理过 如果没有处理过 就处理 判重
处理过了 就return 0