1、Zobrist Hash(By 15-Puzzle problem)
目的:使用Zobrist Hash判断N-Pullze棋局新生成的状态是否存在;
如何从一个状态的Zobrist值得到其后继状态的Zobrist值;
Zobrist Hash思想:
Zobrist hash是一种专门针对棋类游戏而提出来的编码方式, Zobrist哈希通过一种特殊的置换表,也就是对棋盘上每一位置的各个可能状态赋予一个编码索引值,来实现在极低冲突率的前提下在一个整型数据上对棋盘进行编码。
以15-Puzzle为例说明:
题意:4*4棋盘,由15个方块和一个空位组成。15个棋子,每个棋子上标有1至15的某一数字;空格用0表示;空格周围的棋子可以移动到空格中。移动方块,让所有的方块按照数字的顺序排列。(以下讨论将0也算入其中)
一共4x4x16种组合;设定三维数组
Z[4][4][16] //每一个产生一个随机数,至少保证是32位的长度(即32bit),最好是64位。一般我们设定为32位,很多时候为了校验还会再设定一个64位的随机数。
//Z[0][0][0]——Z[0][0][15]的值表示0-15在(0,0)的每个状态的随机数;
对于一个特定的棋局,将每个单位的状态对应的随机数作异或(^)运算,所得值即为Hash值;
对于生成的初始状态,判断新生成的状态是否有与原来的生成的初始状态的Hash值是否一致。若一致,则新生成的初始状态已经存在。
对于判断是否与某历史状态是否存在同理。
如何从一个状态的Zobrist值得到其后继状态的Zobrist值?
当前状态会得到一个Zobrist值a;
比如(0,0)位置上是8,(0,1)位置上是0;
操作:将8移到(0,1);
0到(0,0);
//原来键值里异或过Z[0][0][8] 和 Z[0][1][0];
//再次异或这个值,棋子即从键值中删除;再异或新的状态;所以后继状态的Zobrist为
a1=a^Z[0][0][8]^Z[0][1][0]^Z[0][0][0]^Z[0][1][8];
参考:https://www.cnblogs.com/IThaitian/p/3616507.html
&&队友的代码就不贴了
2、N-Puzzle问题的可解性判断
末状态逆序对:0
初始状态:8-Puzzle ;
3x3; 3为奇数;
上下每交换一次逆序数改变偶次数,左右交换不变
初始状态的逆序数与末状态逆序数奇偶性相同则可达;
15-Puzzle;
4x4; 4为偶数;
上下每交换一次逆序数改变奇次数,左右交换不变;
初始状态的逆序数+初始0到最低行的行距(0最后在最低行)与末状态奇偶性相同即可互相到达;
延申:MxN Puzzle问题同理
逆序数求法:归并排序&树状数组
归并排序原理:分治法
图来源:https://www.cnblogs.com/chengxiao/p/6194356.html
合并:
Code:
#include<bits/stdc++.h>
#define maxn 100
int a[maxn],b[maxn];
using namespace std;
void merge_sort(int l,int r){
if(r-l>0){
int mid=(l+r)/2;
int i=l;
int p=l;
int q=mid+1;
merge_sort(l,mid);
merge_sort(mid+1,r);
while(p<=mid||q<=r){ //左右两部分只要有一部分不为空
if(q>r||(p<=mid&&a[p]<=a[q])){
b[i++]=a[p++];//q>r 即将左边剩的复制到数组,否则比较
}
else{
b[i++]=a[q++]; //将右半数组复制到辅助数组
}
}
for(i=l;i<=r;i++){
a[i]=b[i]; //拷贝
}
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
merge_sort(1,n);
for(int i=1;i<n;i++){
printf("%d ",a[i]);
}
cout<<a[n]<<endl;
return 0;
}
求逆序对原理:
左右两部分合并时,当右指针数字小于左指针的时候,他会比从左指针开始到中间的元素个数个逆序对。也就是mid-l+1;
所以添加一行代码即可求出逆序对:
#include<bits/stdc++.h>
#define maxn 100
int a[maxn],b[maxn];
int cnt;
using namespace std;
void merge_sort(int l,int r){
if(r-l>0){
int mid=(l+r)/2;
int i=l;
int p=l;
int q=mid+1;
merge_sort(l,mid);
merge_sort(mid+1,r);
while(p<=mid||q<=r){ //左右两部分只要有一部分不为空
if(q>r||(p<=mid&&a[p]<=a[q])){
b[i++]=a[p++];//q>r 即将左边剩的复制到数组,否则比较
}
else{
b[i++]=a[q++]; //将右半数组复制到辅助数组
cnt+=mid-p+1;
}
}
for(i=l;i<=r;i++){
a[i]=b[i]; //拷贝
}
}
}
int main(){
int n;
cnt=0;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
merge_sort(1,n);
for(int i=1;i<n;i++){
printf("%d ",a[i]);
}
cout<<a[n]<<endl;
cout<<cnt<<endl;
return 0;
}
树状数组求逆序对:
树状数组原理:
参考https://blog.youkuaiyun.com/Small_Orange_glory/article/details/81290634
Code:
//单点更新
void update(int x,int y,int n){
for(int i=x;i<=n;i+=lowbit(i)) //x为更新的位置,y为更新后的数,n为数组最大值
c[i] += y;
}
//区间查询
int getsum(int x){
int ans = 0;
for(int i=x;i;i-=lowbit(i))
ans += c[i];
return ans;
}
求逆序对原理:
https://blog.youkuaiyun.com/ssimple_y/article/details/53744096
#include<bits/stdc++.h>
#define maxn 1007
using namespace std;
int aa[maxn];//离散化后的数组
int c[maxn]; //树状数组
int n;
struct Node
{
int v;
int order;
}a[maxn];
bool cmp(Node a, Node b)
{
return a.v < b.v;
}
int lowbit(int k)
{
return k&(-k); //基本的lowbit函数
}
void update(int t, int value)
{ //即一开始都为0,一个个往上加(+1),
int i;
for (i = t; i <= n; i += lowbit(i))
c[i] += value;
}
int getsum(int t)
{ //即就是求和函数,求前面和多少就是小于它的个数
int i, sum = 0;
for (i = t; i >= 1; i -= lowbit(i))
sum += c[i];
return sum;
}
int main()
{
int i;
while (scanf("%d", &n), n)
{
for (i = 1; i <= n; i++) //离散化
{
scanf("%d", &a[i].v);
a[i].order = i;
}
sort(a + 1, a + n + 1,cmp);//从1到n排序,cmp容易忘
memset(c, 0, sizeof(c));
for (i = 1; i <= n; i++)
aa[a[i].order] = i;
int ans = 0;
for (i = 1; i <= n; i++)
{
update(aa[i], 1);
ans += i - getsum(aa[i]); //减去小于的数即为大于的数即为逆序数
}
printf("%d\n", ans);
}
return 0;
}