JavaScript中国象棋程序(7) - 置换表
“JavaScript中国象棋程序” 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序。这是教程的第2节。
这一系列共有9个部分:
这一节主要介绍置换表。正如象棋百科全书网所说的,没有置换表,就称不上是完整的计算机博弈程序。
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) - 置换表的更多相关文章
- JavaScript中国象棋程序(0) - 前言
“JavaScript中国象棋程序” 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.希望通过这个系列,我们对博弈程序的算法有一定的了解.同时,我们也将构建出一个不错的中国象棋程序 ...
- JavaScript中国象棋程序(1) - 界面设计
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第1节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- JavaScript中国象棋程序(2) - 校验棋子走法
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第2节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- JavaScript中国象棋程序(3) - 电脑自动走棋
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第3节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- JavaScript中国象棋程序(4) - 极大极小搜索算法
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第4节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- JavaScript中国象棋程序(5) - Alpha-Beta搜索
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第5节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- JavaScript中国象棋程序(6) - 克服水平线效应、检查重复局面
"JavaScript中国象棋程序" 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序.这是教程的第6节. 这一系列共有9个部分: 0.JavaScript中国象 ...
- JavaScript中国象棋程序(8) - 进一步优化
在这最后一节,我们的主要工作是使用开局库.对根节点的搜索分离出来.以及引入PVS(Principal Variation Search,)主要变例搜索. 8.1.开局库 这一节我们引入book.js文 ...
- 中国象棋程序的设计与实现(六)--N皇后问题的算法设计与实现(源码+注释+截图)
八皇后问题,是一个古老而著名的问题,是回溯算法的典型例题. 该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一列 ...
随机推荐
- IOS开发中响应者链
在IOS开发中,有时候会遇到如下情况:在页面1上有一个RedView,在RedView上有一个GreenView,在GreenView上有一个button,这些view的创建代码如下: 1.AppDe ...
- Statemen接口对象
利用Statement接口实现数据表的更新和查询操作 -取得Statement接口对象:Statement createStatement(int resultSetType, int resultS ...
- Web前端开发中的各种CSS规范
Reference: http://yusi123.com/2866.html 一.文件规范 1.文件均归档至约定的目录中(具体要求以豆瓣的CSS规范为例进行讲解): 所有的CSS分为两大类:通用类和 ...
- pandas 按照列A分组,将同一组的列B求和,生成新的Dataframe
对于pandas中的Dataframe,如果需要按照列A进行分组,将同一组的列B求和,可以通过下述操作完成: df = df.groupby(by=['column_A'])['column_B']. ...
- Intent的概念及应用(二)
------siwuxie095 1.Intent过滤器 intent-filter 相关选项 如果多个Activity拥有同样的action,在启动时这个action时的情况: 首先在LearnIn ...
- DOMContentLoaded
setTimeout(function(){ alert("settimeout"); },0); EventUtil.addHandler(window, "DOMCo ...
- iOS 专题 之 界面开发 之 控件
iOS 之 UIViewController iOS 之 Navagation Button iOS 之 UIButton iOS 之 UITextField iOS 之 UIStackView iO ...
- Jenkins搭建Windows slave 环境
因为之前的接口测试平台是在windows上,后来换了linux,花了半天时间也没有成功,就想着建立一个windows的节点曲线救国 创建WindowsSlave 首先要保证windows和sl ...
- 避免Node.js中回调地狱
为了解决这个阻塞问题,JavaScript严重依赖于回调,这是在长时间运行的进程(IO,定时器等)完成后运行的函数,因此允许代码执行经过长时间运行的任务. downloadFile('example. ...
- Linux 系统分区
合理的系统分区,便于后期管理和提高文件的搜索速度 分区格式说明 linux分区不同于windows,linux下硬盘设备名为(IDE硬盘为hdx(x为从a—d)因为IDE硬盘最多四 个,SCSI,SA ...