天天看点

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

我们很容易明白,在alpha-beta剪枝算法中,会遇到重复的棋局。如下面的两个图的走法是一样的,不必要将两个局面的得分都计算一次。

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数
QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

因此需要将棋盘的信息保存起来以备后续使用,这就是置换表。在棋类游戏中经常使用的就是zobrist哈希。

zobrist哈希并不是一种最好的哈希算法,但是确实最高效的。有时候我们必须在最好与效率之间做出选择。

zobrist是一个人的名字,源自他1970的年的一片论文

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

试想一下你如何有效的存储一个棋盘的信息呢?zobrist提供了一种办法:棋盘的一个唯一的标识就是棋盘的所有状态的异或值。

棋盘的每一个状态实际就对应着一个随机数。在五子棋中,我们忽视空白,将每一个位置的状态标示为黑棋或白棋两种状态。

第一步:是需要初始化每一个状态的随机数。设置两个数组。对于随机数的产生使用了Twister算法,开源项目:www.agner.org/random

其实就是用别人写好的,调用一下函数,不要想得那么复杂。rand函数只能生成五位随机数,不推荐使用。

int WriteZobrist[15][15];      
int BlackZobrist[15][15];      
void  Game::InitZobrist()      
{      
int seed = (int)time(0);            // random seed      
// choose one of the random number generators:      
CRandomMersenne RanGen(seed);       // make instance of random number generator      
for(int i= 0;i<15;i++)      
{      
for(int j = 0;j<15;j++)      
{      
WriteZobrist[i][j]=RanGen.IRandom(0,INT_MAX);      
     BlackZobrist[i][j]=RanGen.IRandom(0,INT_MAX);      
}      
}      
}      

接下来对于这个棋局的哈希值,是多少呢?

黑棋在(6,6),(8,8)点

白棋在(6,8),(8,6)点

hashcode =WriteZobrist[6][8]^WriteZobrist[8][6]^BlackZobrist[6][6]^BlackZobrist[8][8]

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

就这么简单啊兄弟。由于异或的特性,因此,在五子棋中,下一个子后,只需要用hashcode异或一次就可以啦。

接下来就是考虑一下hash冲突,由于hash冲突问题常常需要加上一个64位的校验位。来保证这个棋局是哈希表中的棋局,减少冲突。

longlong WZobrist[15][15];      
longlong BZobrist[15][15];      

第二、一定要注意,棋局转到hashcode只是第一步,第二步是要把hashcode转换为地址存到hash表中。这两步都可能会发生冲突。把hashcode转换为地址存到hash表中简单的办法就是除表的大小取余数。因此,哈希表越大冲突就越小。

第三、一定要存储深度,我们来看一段zobrist在1970年的描述:

这段话就说明了深度是重要的,因为在博弈算法中,更深的分数意味着更稳健,因此,在第3层的局面可以作为di5层局面的参考,因为第3层搜索得更深。这段话同时说明了在剪枝中如何使用。

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

第四、在剪枝中还需要考虑一些额外的极大值极小值问题。着实有点繁琐。因此必须要存储极大值极小值信息。

一段伪代码:

QT五子棋详解之九:置换表,zobrist哈希,生成32位和64位随机数

最后要说的是,置换表用在五子棋上没多大的效率提升,一般是用在象棋等游戏上。