“JavaScript中国象棋程序” 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序。这是教程的第2节。

这一系列共有9个部分:

0、JavaScript中国象棋程序(0)- 前言

这一节主要介绍置换表。正如象棋百科全书网所说的,没有置换表,就称不上是完整的计算机博弈程序。

7.1、置换表

上图所示的搜索树中,局面A出现了3次,程序也搜索了3次,这浪费了很多时间。何不把A的搜索结果保存在表里,后面再搜索到A时直接查表取值,避免重复搜索呢?保存搜索结果的表,就是置换表。由于哈希表的读写速度很快,通常置换表就由哈希表来实现。

7.1.1、哈希表

哈希表是用散列方法存储的线性表。它以节点的关键字K为自变量,通过一个确定的函数关系h,计算出对应的函数值h(K),然后把这个值解释为节点的存储地址,将节点存入h(K)所指的存储位置上。在查找时,根据要查找的关键字用同一函数h计算出地址,再到相应的单元里查找要找的节点。函数h(K)称为散列函数或哈希函数。

如:11个元素的关键字分别为18,27,1,20,22,6,10,13,41,15,25。用11个连续存储空间来存放,选取关键字与元素位置间的函数为h(K) = key mod 11。(mod是取余运算)

7.1.2、存储

在我们的程序中,关键字K就是上节已经用到的Zobrist校验码。哈希函数同样是取余运算:

h(K) = Zobrist % TableSize

其中,TableSize是哈希表的长度。

但是这个函数在速度上有个瓶颈,因为“电脑一做除法就成了傻瓜”。因此,TableSize最好是2的整数次幂,这样就能把“取余运算”转化为“按位与运算”。比如取TableSize = 16,那么有:

7 mod 16 = 7,  17 mod 16 = 1,  25 mod 16 = 9

转换为按位与运算就有:

7 & 15 = 7,  17 & 15 = 1,  25 & 15 = 9

也就是说,和15进行与运算,就是对16取余。(可参考这篇文章

因此,h(K) = Zobrist & (TableSize - 1),其中TableSize是2的整数次幂。

每个哈希表项存储的内容包括:深度(depth)、节点类型(flag)、分值(vl)、最佳走法(mv)、Zobrist Lock校验码。后面会介绍这些字段的作用。

7.1.3、冲突处理

以TableSize = 16为例,

7 mod 16 = 7

23 mod 16 = 7

因此,通过哈希函数算得的位置可能存在冲突。我们处理冲突的方法很简单——深度优先的替换。如果新节点深度高于旧节点,直接用新节点替换旧结点;否则,保留旧节点不变。

7.1.4、查找

由于Zobrist校验码不足以保证局面的唯一性,我们对每个局面都生成一个Zobrist Lock校验码(生成方式与Zobrist校验码一样),并保存在哈希表表项中。

查找置换表时,首先根据Zobrist校验码计算出哈希表中的地址,然后再比较Zobrist Lock校验码是否一致,一致的话才能说明是同一局面。

7.2、节点类型

(a)alpha节点                               (b)beta节点                                    (c)PV节点

如上图所示,(10,20)是指搜索到C节点时,alpha为10,beta为20。

在图(a)中,所有子节点的值均小于alpha值10,最后C点取值为9,C点称为alpha点。

在图(b)中,节点C通过走法1到达D1取值为24,超过了beta值20,剪去了D2和D3两个分支,同时C点取值24。24并不是C点的真实值,我们只知道C点的值不比24小。这样的C点称为beta节点。

在图 (c)中,C点的取值为14,它是所有子节点中的最大值,反映了C点的真是情况,所以C点称为PV节点(Principal Variation)。

如果从哈希表中是一个PV节点,那么当前所搜索节点的值,就是哈希表中存储的值。

如果哈希表中是个beta节点,值为value。由于beta节点会发生剪枝,value不是一个准确值。只能说明当前搜索节点的值,不小于value。

如果哈希表中是个alpha节点,值为value。对于不超出边界的Alpha-Beta搜索,value显然不是个准确值,只能说明当前搜索节点的值不大于value。对于超出边界的Alpha-Beta搜索,例如上面的图(a),我个人觉得,9就是节点C的真实情况吧。让我不解的是,在程序中,使用的正是超出边界的搜索,却没有把alpha节点的值作为准确值来处理。

7.3、杀棋分数调整

在第5节,我们已经考虑了杀棋的分值,把输棋的分值与搜索层数结合起来:

输棋分值 = -MATE_VALUE + 搜索层数

赢棋分值 = MATE_VALUE - 搜索层数

现在使用置换表之后,由于相同的局面,可能位于不同的层数,所以不能再把这个调整后的分值存入置换表。我们的做法是,存入置换表时,存储与层数无关的分值(比如-MATE_VALUE);读取置换表时,再调整为与层数相关的分值(-MATE_VALUE + 当前搜索层数)。

7.4、杀手走法

第5节我们介绍了历史表,将一些好的走法(beta节点引发剪枝的走法、PV节点估值最好的走法)保存到历史表。根据国家象棋的经验,一个节点好的走法,在它兄弟节点也很可能就是好的走法。但是兄弟节点的走法,在当前节点下未必能走,所以在尝试杀手走法以前先要对它进行走法合理性的判断。

如何保存和获取“兄弟节点中产生截断的走法”呢?我们可以把这个问题简单化——距离根节点步数(distance)同样多的节点,彼此都称为“兄弟”节点,换句话说,亲兄弟、堂表兄弟以及关系更疏远的兄弟都称为“兄弟”。

我们可以把距离根节点的步数(distance)作为索引值,构造一个杀手走法表。我们的程序每个杀手走法表项存有两个杀手走法,走法一比走法二优先:存一个走法时,走法二被走法一替换,走法一被新走法替换;取走法时,先取走法一,后取走法二。

7.5、优化走法顺序

利用各种信息渠道(如置换表、杀手走法、历史表等)来优化走法顺序的手段称为“启发”。之前我们只用历史表作启发,但从这个版本开始,我们采用了多种启发方式:

1、如果置换表中有过该局面的数据,但无法完全利用,那么多数情况下它是浅一层搜索中产生截断的走法,我们可以首先尝试它;

2、然后是两个杀手走法(如果其中某个杀手走法与置换表走法一样,那么可以跳过);

3、然后生成全部走法,按历史表排序,再依次搜索(可以排除置换表走法和两个杀手走法)。

7.6、核心代码说明

本节的代码可以在 Github 下载,也可以直接clone

git clone -b step-7 https://github.com/Royhoo/write-a-chinesechess-program

Search中新增或修改的主要属性和方法:

(1)、hashMask

hashMask = 哈希表长度 - 1

用于将哈希函数的“取余运算”,转化为“按位与运算”。

(2)、recordHash(flag, vl, depth, mv)

记录哈希表

(3)、probeHash(vlAlpha, vlBeta, depth, mv)

查询哈希表

(4)、setBestMove(mv, depth)

该函数之前只更新历史表,目前要同时更新杀手走法表。

MoveSort中修改的方法:

(1)、next()

该方法是获取排序后的一个走法,目前加入置换表走法、杀手走法、历史表走法三种启发。

JavaScript中国象棋程序(7) - 置换表的更多相关文章

  1. JavaScript中国象棋程序(0) - 前言

    “JavaScript中国象棋程序” 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.希望通过这个系列,我们对博弈程序的算法有一定的了解.同时,我们也将构建出一个不错的中国象棋程序 ...

  2. JavaScript中国象棋程序(1) - 界面设计

    "JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第1节. 这一系列共有9个部分: 0.JavaScript中国象 ...

  3. JavaScript中国象棋程序(2) - 校验棋子走法

    "JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第2节. 这一系列共有9个部分: 0.JavaScript中国象 ...

  4. JavaScript中国象棋程序(3) - 电脑自动走棋

    "JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第3节. 这一系列共有9个部分: 0.JavaScript中国象 ...

  5. JavaScript中国象棋程序(4) - 极大极小搜索算法

    "JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第4节. 这一系列共有9个部分: 0.JavaScript中国象 ...

  6. JavaScript中国象棋程序(5) - Alpha-Beta搜索

    "JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第5节. 这一系列共有9个部分: 0.JavaScript中国象 ...

  7. JavaScript中国象棋程序(6) - 克服水平线效应、检查重复局面

    "JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第6节. 这一系列共有9个部分: 0.JavaScript中国象 ...

  8. JavaScript中国象棋程序(8) - 进一步优化

    在这最后一节,我们的主要工作是使用开局库.对根节点的搜索分离出来.以及引入PVS(Principal Variation Search,)主要变例搜索. 8.1.开局库 这一节我们引入book.js文 ...

  9. 中国象棋程序的设计与实现(六)--N皇后问题的算法设计与实现(源码+注释+截图)

    八皇后问题,是一个古老而著名的问题,是回溯算法的典型例题. 该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一列 ...

随机推荐

  1. S3C2440硬件IIC详解

    S3C2440A RISC微处理器可以支持一个多主控IIC 总线串行接口.一条专用串行数据线(SDA)和一条专用串行时钟线(SCL)传递连接到IIC总线的总线主控和外设之间的信息.SDA和SCL线都为 ...

  2. UVa 10473 - Simple Base Conversion

    题目大意:十进制与十六进制之间的相互转换. #include <cstdio> int main() { #ifdef LOCAL freopen("in", &quo ...

  3. 动态添加试题选项按钮 radioButton(一)

    最近在做WebView加载试题的功能,但是选项按钮如果放的WebView中,点击时反应很慢.于是把选项用原生的RadioButton,而试题题目和答案放在WebView中.但是选项的个数不确定,所以需 ...

  4. python2与python3编码问题

    python2: UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 33: 解决办法: 在报错的页面添加代码:  ...

  5. linux设置好IP后,可以访问内网,不能访问外网

    1,设置网卡,ip vi /etc/sysconfig/network-scripts/ifcfg-eth0 DEVICE=eth0 #描述网卡对应的设备别名,例如ifcfg-eth0的文件中它为et ...

  6. web前端好学吗?

    最近这段时间许多学生讨论关于WEB前端工程师这个职位的问题.比如:关于前端难不难?好不好找工作?有没有用?好不好学?待遇好不好?好不好转其他的职位? 针对这个问题,课工场露露老师想跟大家谈谈自己对前端 ...

  7. c#和java中的方法覆盖——virtual、override、new

    多态和覆盖 多态是面向对象编程中最为重要的概念之一,而覆盖又是体现多态最重要的方面.对于像c#和java这样的面向对象编程的语言来说,实现了在编译时只检查接口是否具备,而不需关心最终的实现,即最终的实 ...

  8. 详解Grunt插件之LiveReload实现页面自动刷新(两种方案)

    http://www.jb51.net/article/70415.htm    含Grunt系列教程 这篇文章主要通过两种方案详解Grunt插件之LiveReload实现页面自动刷新,需要的朋友可以 ...

  9. Intel为什么做不好手机CPU?

    Intel大名鼎鼎,在CPU界无人不知无人不晓,然而在当前主流的手机CPU市场上却是远远落后日本的ARM公司,这到底是Intel技术不足,还是ARM过于强大呢,今天我们就来探讨一下. 故事要从2006 ...

  10. Java-io流入门到精通详细总结

    IO流:★★★★★,用于处理设备上数据. 流:可以理解数据的流动,就是一个数据流.IO流最终要以对象来体现,对象都存在IO包中. 流也进行分类: 1:输入流(读)和输出流(写). 2:因为处理的数据不 ...