博弈基本技术——置换表




《对弈程序基本技术》专题
 
置换表
 
Bruce Moreland / 文
  一个多功能的数据结构     国际象棋的搜索树可以用图来表示,而置换结点可以引向以前搜索过的子树上。置换表可以用来检测这种情况,从而避免重复劳动。如果“ 1. e4 d6 2. d4”以后的局面已经搜索过了,那就没有必要再搜索“ 1. d4 d6 2. e4”以后的局面了。   这个原因可能鼓舞着早期的电脑国际象棋程序的设计师们,而现在事实上这还是置换表的次要用途。在某些局面,例如在没有通路兵的王兵残局中,检查到的置换的数量是惊人的,以至于搜索可以在短达时间内达到很深的深度。   省去重复的工作,这是置换表的一大特色,但是在一般的中局局面里,置换表的另一个作用更为重要。每个散列项里都有局面中最好的着法,我在“ 迭代加深”这一章里解释过,首先搜索好的着法可以大幅度提高搜索效率。因此如果你在散列项里找到最好的着法,那么你首先搜索这个着法,这样会改进你的着法顺序,减少分枝因子,从而在短的时间内搜索得更深。   实现     主置换表是一个散列数组,每个散列项看上去像这样:   #define hashfEXACT 0 #define hashfALPHA 1 #define hashfBETA 2 typedef struct tagHASHE {  U64 key;  int depth;  int flags;  int value;  MOVE best; } HASHE;     这个散列数组是以“ Zobrist键值”为指标的。你求得局面的键值,除以散列表的项数得到余数,这个散列项就代表该局面。由于很多局面都有可能跟散列表中同一项作用,因此散列项需要包含一个校验值,它可以用来确认该项就是你要找的。通常校验值是一个 64位的数,也就是上面那个例子的第一个域。   你从搜索中得到结果后,要保存到散列表中。如果你打算用散列表来避免重复工作,那么重要的是记住搜索有多深。如果你在一个结点上搜索了 3层,后来又打算做 10层搜索,你就不能认为散列项里的信息是准确的。因此子树的搜索深度也要记录。   在 Alpha-Beta搜索中,你很少能得到搜索结点的准确值。 Alpha和 Beta的存在有助你裁剪掉没有用的子树,但是用 Alpha-Beta有个小的缺点,你通常不会知道一个结点到底有多坏或者有多好,你只是知道它足够坏或足够好,从而不需要浪费更多的时间。   当然,这就引发了一个问题,散列项里到底要保存什么值,并且当你要获取它时怎样来做。答案是储存一个值,另加一个标志来说明这个值是什么含义。在我上面的例子中,比方说你在评价域中保存了 16,并且在标志域保存了“ hashfEXACT”,这就意味着该结点的评价是准确值 16;如果你在标志域中保存了“ hashfALPHA”,那么结点的值最多是 16;如果保存了“ hashfBETA”,这个值就至少是 16。   当你在搜索中遇到特定情况时,很容易决定评价和标志应该保存哪些内容。然而避免错误是非常重要的,散列表是非常容易犯错误的,而且一旦犯下错误就很难捕捉出来。   我的散列项的最后一个域,保存着上次搜索到这个局面时的最佳着法。有时我没有得到最佳着法,比如任何低出边界的情况 (返回一个小于或等于 Alpha的值 ),而其他情况必定有最佳着法,比如高出边界的情况 (返回一个大于或等于 Beta的值 )。 【译注:只有叶子结点才没有最佳着法,即便是 Alpha 结点,所有的着法都是差的,也应该从中找一个最好的着法,它对更深一层的搜索会带来很大的好处。】   如果找到最佳着法,那么它应该首先被搜索。   下面是示范程序,是根据 Alpha-Beta函数修改的,改动的地方用醒目的字标出:   int AlphaBeta(int depth, int alpha, int beta) {  int hashf = hashfALPHA;  if ((val = ProbeHash(depth, alpha, beta)) != valUNKNOWN) {   // 【valUNKNOWN必须小于-INFINITY或大于INFINITY,否则会跟评价值混淆。】   return val;  }  if (depth == 0) {   val = Evaluate();   RecordHash(depth, val, hashfEXACT);   return val;  }  GenerateLegalMoves();  while (MovesLeft()) {   MakeNextMove();   val = -AlphaBeta(depth - 1, -beta, -alpha);   UnmakeMove();   if (val >= beta) {    RecordHash(depth, beta, hashfBETA);    return beta;   }   if (val > alpha) {    hashf = hashfEXACT;    alpha = val;   }  }  RecordHash(depth, alpha, hashf);  return alpha; }     以下就是两个新的函数的代码:   int ProbeHash(int depth, int alpha, int beta) {  HASHE *phashe = &hash_table[ZobristKey() % TableSize()];  if (phashe->key == ZobristKey()) {   if (phashe->depth >= depth) {    if (phashe->flags == hashfEXACT) {     return phashe->val;    }    if ((phashe->flags == hashfALPHA) && (phashe->val <= alpha)) {     return alpha;    }    if ((phashe->flags == hashfBETA) && (phashe->val >= beta)) {     return beta;    }   }   RememberBestMove();  }  return valUNKNOWN; }   void RecordHash(int depth, int val, int hashf) {  HASHE *phashe = &hash_table[ZobristKey() % TableSize()];  phashe->key = ZobristKey();  phashe->best = BestMove();  phashe->val = val;  phashe->hashf = hashf;  phashe->depth = depth; }     你所看到的代码,并不像航天科学一样准确,而是很可能有错误的,而且细节上的问题我还没有讨论。如果你的程序中有错误,或许就是很严重的错误。   【以上代码有个速度上的瓶颈,即“ ZobristKey() % TableSize() ”这个表达式。由于“电脑一做除法就成了傻瓜”,所以“ TableSize ”最好是一个 2n 的常量,只有当除数是 2n 时除法才可以由右移指令取代。最好的方法是设一个“ TableSizeMask ”的变量:   int TableSizeMask = TableSize() - 1; HASHE *phashe = &hash_table[ZobristKey() & TableSizeMask];     而这里“ TableSize() ”也必须是 2n 。正是这个道理,在很多可以设定置换表大小的国际象棋程序中,允许的设定值总是呈倍数增长的,要么是 3M 6M 12M 24M 等等 ( 如果每个散列项有 12 字节 ) ,要么是 4M 8M 16M 32M 等等 ( 如果每个散列项有 16 字节 ) 。】   替换策略     最主要的细节就包括,什么时候该覆盖散列项。在上面的例子中,我用了“始终替换”的策略,即简单地覆盖已经存在的值。这或许不是最好的策略,事实上已经有大量的工作试图找出哪个策略是最好的。   另一个策略是“同样深度或更深时替换”。除非新局面的深度大于或等于散列表中已经有的值,否则已经存在的结点将被保留。   还有很多试验的余地。 1994年我在 Usenet(新闻组网络系统 )的新闻组 rec.games.chess(如今是 rec.games.chess.computer)上问了这个问题,得到了 Ken Thompson的答复。    他的回答是使用两个散列表。一个使用“始终替换”策略,另一个使用“同样深度或更深时替换”。当你做试探时,两个散列表都去试探,如果其中一个可以产生 截断,那就可以了。如果两者都不能产生截断,那么你可能至少得到一个最佳着法,实际上更多的可能是得到两个不同的着法,两者都应该首先 (或第二个 )尝试。   记录的时候,你只要简单地根据替换策略来执行。   如果你使用“同样深度或更深时替换”的策略,那么你的散列表可能最终会被过期的但很深的结点所占满。解决方案就是每次你走棋时都清除散列表,或者在散列项中加入“顺序”这个域,从而使这个策略变成变成“同样深度,或更深,或原来是旧的搜索,才替换”。   我在我的程序 Ferret中使用了 Thompson的策略,并且运行得很好。另一个程序 Gerbil也使用这个策略,你可以去看它的源代码。   【根据译者研究的结果,只用“深度优先覆盖”策略 ( 即“同样深度或更深时替换” ) ,效果会比“始终替换”好得多,而代码则并不复杂,只有醒目的部分是新增的:   void RecordHash(int depth, int val, int hashf) {  HASHE *phashe = &hash_table[ZobristKey() & (TableSize() - 1)];  if (phashe->hashf != hashfEMPTY && phashe->depth > depth) {   return;  }  phashe->key = ZobristKey();  phashe->best = BestMove();  phashe->val = val;  phashe->hashf = hashf;  phashe->depth = depth; }     如果使用这个代码,那么每走一步以前都必须把散列表中所有的标志项置为“ hashfEMPTY ”。】   不稳定性的问题     当你用置换表时,如果你允许搜索过程根据散列项来截断,那就会产生另一个问题,你的搜索会受“ 不稳定性”的捆扰。   不稳定性至少是由以下因素引起的:    1. 你可能在做 6层的搜索,但是如果你在散列项中得到 10层搜索的结果,就可能根据这个值来截断。在后来的搜索中,这个散列项被覆盖了,因此你在这个结点上得到了两个不同的值。    2. Zobrist键值无法记录到达结点的线路,这个结点上不是每条线路都有相同结果的。如果某条线路遇到重复局面,那么散列项的值就会跟路线有关。因为重复局面会导致和局的分值,或者至少不一样的分值。   就我所知,还没有什么办法能处理这些问题。   【另外,如果搜索过程中找到杀棋,那么评价值会接近“ INFINITY ”或“ - INFINITY ”,此时记录散列表时不能简单地记录这些评价值,在后面介绍的“ 胜利局面 ”的处理中,会谈到这个问题。】     原文: http://www.seanet.com/~brucemo/topics/hashing.htm   译者:黄晨 ()   类型:全译加译注
  • 上一篇 基本搜索方法——迭代加深
  • 下一篇 高级搜索方法——简介(一)
  • 返 回 象棋百科全书——电脑象棋
  • 转自:http://www.elephantbase.net/computer/search_hashing.htm
    • 3
      点赞
    • 3
      收藏
      觉得还不错? 一键收藏
    • 0
      评论
    中国象棋,位棋盘,Zobrist键值,alpha-beta搜索,置换,局面评价,包含设计说明 摘 要:随着人工智能及计算机硬件的发展,计算机象棋程序的下棋水平也不断地得到提高。20世纪60年代初,麦卡锡提出了alpha-beta修剪算法,把为决定下一个走步而需对棋盘状态空间的搜索量从指数级减少为指数的平方根,大大地提高了机器下棋的水平。IBM的超级计算机“Deep Blue”更是一个神话,让棋迷们神往。本文根据国际象棋程序设计的一些成功经验,提出中国象棋程序设计的一些思路和方法。 关 键 词:中国象棋,位棋盘,Zobrist键值,alpha-beta搜索,置换,局面评价 Abstract:Along with the development of the Artificial Intelligence and computer hardware, the capability of computer chess program have advanced continually.At the beginning of 60s,20th century, McCaxi brought forword alpha-beta pruning algorism which made the chess program advanced more by reducing the order of magnitude of the number of searching nodes deciding next step,named “State Space” from O(Xn) to O(Xn/2). IBM’s super-computer “Deep Blue” is more like a myth for all computer chess fans. In my article, I will describe some ideas and methods of designing Chinese Chess program along with some successful experiences and cases of the Chess. Keywords: Chinese Chess, bit board, zobrist keys, alpha-beta search, transposition table, Evaluation 目 录 引 言 3 第一章 概述 4 1.1 棋盘的标记 4 1.2 棋子的名称 5 1.3 棋谱的记录方法 5 1.4 历史局面的示及存储 7 1.5 棋谱记录文件的格式 8 第二章 基本数据结构——位棋盘 10 2.1 什么是位棋盘 10 2.2 位棋盘的作用 10 2.3 位棋盘的基本运算 12 2.4 Java中位棋盘的实现 13 第三章 基本数据结构——Zobrist键值 17 3.1 比较局面的方法 17 3.2 Zobrist键值的实现方法 17 3.3 Zobrist键值的工作原理及用途 17 3.4 Java中实现Zobrist键值 18 第四章 着法生成 20 4.1伪合法着法的生成 20 4.2 合法着法的生成 25 第五章 搜索算法 29 5.1 最小-最大搜索 29 5.2 Alpha-Beta搜索 33 5.3 迭代加深 36 5.4 置换 37 5.5 其他策略 41 第六章 局面评价函数 47 6.1 评价函数的实现方法 48 6.2 评价函数所需的信息 48 第七章 程序的设计及实现 51 7.1 搜索引擎的实现(engine包) 51 7.2 信息传输机制(message包) 52 7.3 棋子生成(pieces包) 52 7.4 主控模块(main包) 52 附件1:搜索算法主程序SearchMove.java 55 附件2:程序运行界面及功能说明 74
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值