auntyellow 发表于 2005-6-14 22:59:00

解剖大象的眼睛——中国象棋程序设计探索(五):消除水平线效应

<br /><br /><DL>
<DL>
<DL>
<DIV align=center>
<CENTER>
<DT><FONT face=隶书 size=6>解剖大象的眼睛</FONT><FONT size=6><STRONG>——</STRONG></FONT><FONT face=隶书 size=6>中国象棋程序设计探索</FONT> </CENTER></DT></DIV>
<DIV align=center>
<CENTER>
<DT>  </CENTER></DT></DIV>
<DIV align=center>
<CENTER>
<DT>黄晨 <FONT face="Times New Roman">*</FONT> <FONT face="Times New Roman">2005</FONT>年<FONT face="Times New Roman">6</FONT>月 </CENTER></DT></DIV>
<DIV align=center>
<CENTER>
<DT> 
<DT><FONT face=Arial size=5><STRONG>(</STRONG></FONT><FONT face=楷体_GB2312 size=5><STRONG>五</STRONG></FONT><FONT face=Arial size=5><STRONG>) </STRONG></FONT><FONT face=楷体_GB2312 size=5><STRONG>克服水平线效应</STRONG></FONT>
<DT> 
<DT> 
<DT><FONT face=Arial size=4><STRONG>5.1 </STRONG></FONT><FONT face=楷体_GB2312 size=4><STRONG>裁剪算法概述</STRONG></FONT>
<DT> 
<DT>  狭义上的裁剪算法属于申朗的<FONT face="Times New Roman">B</FONT>策略,在象棋程序中用得并不多,主要还是<FONT face="Times New Roman">90</FONT>年代提出的空着裁剪。国际象棋程序<FONT face="Times New Roman">DarkThought</FONT>中使用了“<FONT face="Times New Roman">AEL</FONT>裁剪”<FONT face="Times New Roman">(AEL Pruning)</FONT>算法,其中<FONT face="Times New Roman">A</FONT>是改进的空着裁剪,而吸引人的是<FONT face="Times New Roman">E</FONT>和<FONT face="Times New Roman">L</FONT>部分,为此<FONT face="Times New Roman">ElephantEye</FONT>尝试了这两部分的裁剪,虽然并未大幅度提高搜索效率,但笔者体会不少。
<DT>  著名中国象棋软件《象棋世家》的作者郑明政认为,将<FONT face="Times New Roman">AEL</FONT>算法从国际象棋移植到中国象棋的时候必须格外小心,还列举了算法的三个负面作用:
<DT>  <FONT face="Times New Roman">(1) A</FONT>——适应性空着裁剪<FONT face="Times New Roman">(Adaptive Null-Move Pruning)</FONT>,要小心停着杀和连停着杀,这是由于空着裁剪的不适合无等着局面的本质所致;
<DT>  <FONT face="Times New Roman">(2) E</FONT>——延伸的无效路线裁剪<FONT face="Times New Roman">(Extended Futility Pruning)</FONT>,要小心死子不急吃的问题,因为不吃子的线路表面上是无效线路,因此裁剪后剩下的深度就不足以看到死子不急吃的局面了。
<DT>  <FONT face="Times New Roman">(3) L</FONT>——限制性修剪<FONT face="Times New Roman">(Limited Razoring)</FONT>,要小心弃子杀和弃多子杀,因为子力落后的线路作了修剪,从而无法预测到后面存在的杀局。
<DT>  明白了产生这三个负面作用的原因,也就理解了<FONT face="Times New Roman">AEL</FONT>算法的本质,稍后笔者将介绍如何在运用裁剪技术的时候避免产生负面作用,其中一个重要的手段就是选择性延伸。延伸的处理手法正好跟裁剪相反,但起到的作用却与裁剪相同,因此也是本章的主题之一。
<DT> 
<DT>  谈到裁剪算法,就不得不提到局面的评价问题,因为很多大多数裁剪方法都是建立在评价局面的基础上的。例如空着裁剪在残局中应该自动禁用,这就涉及到残局界定的问题,又如无效路线裁剪要涉及到无效路线如何界定的问题,限制性修剪需要涉及到界定子力落后的问题,这些问题都和局面评价有关。
<DT>  例如,<FONT face="Times New Roman">ElephantEye</FONT>只会评价子力价值,把一个轻子<FONT face="Times New Roman">(</FONT>马或炮<FONT face="Times New Roman">)</FONT>的价值定为<FONT face="Times New Roman">100</FONT>左右,因此裁剪条件的界定都和这个值有关。当程序重写时,轻子价值改变了,裁剪条件也必须作相应的改变。
<DT> 
<DT><FONT face=Arial size=4><STRONG>5.2 </STRONG></FONT><FONT face=楷体_GB2312 size=4><STRONG>带检验的适应性空着裁剪</STRONG></FONT> </DT></DL>
<DL>
<DT>  “适应性空着裁剪”<FONT face="Times New Roman">(Adaptive Null-Move Pruning)</FONT>指的是根据不同情况来调整<FONT face="Times New Roman"><EM>R</EM></FONT>值<FONT face="Times New Roman">(</FONT>裁剪深度<FONT face="Times New Roman">)</FONT>的算法,它首先由国际象棋程序<FONT face="Times New Roman">DarkThought</FONT>的作者<FONT face="Times New Roman">Ernst Heinz</FONT>发表在<FONT face="Times New Roman">1999</FONT>年的<FONT face="Times New Roman">ICCA</FONT>杂志上<FONT face="Times New Roman">(</FONT>参阅<FONT face="Times New Roman">Heinz EA: <EM><STRONG>Adaptive Null-Move Pruning</STRONG></EM><EM>, ICCA J.</EM> 22 (3): 123-132, <STRONG>1999</STRONG></FONT>,可从互联网上找到<FONT face="Times New Roman">)</FONT>。其内容可以概括为:
<DT>  <FONT face="Times New Roman">(1) </FONT>深度小于或等于<FONT face="Times New Roman">6</FONT>时,用<FONT face="Times New Roman"><EM>R</EM> = 2</FONT>的空着裁剪进行搜索;
<DT>  <FONT face="Times New Roman">(2) </FONT>深度大于<FONT face="Times New Roman">8</FONT>时,用<FONT face="Times New Roman"><EM>R</EM> = 3</FONT>;
<DT>  <FONT face="Times New Roman">(3) </FONT>深度是<FONT face="Times New Roman">7</FONT>或<FONT face="Times New Roman">8</FONT>时,如果每方棋子都大于或等于<FONT face="Times New Roman">3</FONT>个,则用 <FONT face="Times New Roman"><EM>R</EM> = 3</FONT>,否则用 <FONT face="Times New Roman"><EM>R</EM> = 2</FONT>。
<DT>  <FONT face="Times New Roman">ElephantEye</FONT>做空着裁剪时,也用了<FONT face="Times New Roman"><EM>R</EM> = 2<SUP><U>6/8</U></SUP>3</FONT>法则,但是<FONT face="Times New Roman">ElephantEye</FONT>并没有判断子数的功能,因此“每方棋子都大于或等于<FONT face="Times New Roman">3</FONT>个”改成了“每方子力价值总和大于<FONT face="Times New Roman">3</FONT>个轻子的价值”,因此确定<FONT face="Times New Roman"><EM>R</EM></FONT>的函数是这样的:
<DD> 
<DD>const int EndgameMargin = 320;
<DD>int RAdapt(int Depth) {
<DD> if (Depth &lt;= 6) {
<DD>  return 2;
<DD> } else if (Depth &lt;= 8) {
<DD>  return Evalue &lt; EndgameMargin || Evalue &lt; EndgameMargin ? 2 : 3;
<DD> } else {
<DD>  return 3;
<DD> }
<DD>}
<DT> 
<DT>  “带检验的空着裁剪”<FONT face="Times New Roman">(Verified Null-Move Pruning)</FONT>指的是检验空着裁剪是否安全的算法,它首先由<FONT face="Times New Roman">Tabibi</FONT>发表在<FONT face="Times New Roman">2002</FONT>年的<FONT face="Times New Roman">ICGA(</FONT>原<FONT face="Times New Roman">ICCA)</FONT>杂志上<FONT face="Times New Roman">(</FONT>参阅<FONT face="Times New Roman">Tabibi OD, Netanyahu NS: <EM><STRONG>Verified Null-Move Pruning</STRONG></EM><EM>, ICGA J.</EM> 25 (3): 153-161, <STRONG>2002</STRONG></FONT>,可以从互联网上找到<FONT face="Times New Roman">)</FONT>。其内容可以概括为:
<DT>  <FONT face="Times New Roman">(1) </FONT>用<FONT face="Times New Roman"><EM>R</EM> = 3</FONT>的空着裁剪进行搜索;
<DT>  <FONT face="Times New Roman">(2) </FONT>如果高出边界,那么做浅一层的搜索<FONT face="Times New Roman">(</FONT>这就意味着一层的搜索是无法做带验证的空着裁剪的<FONT face="Times New Roman">)</FONT>;
<DT>  <FONT face="Times New Roman">(3) </FONT>做浅一层的搜索时,子结点用<FONT face="Times New Roman"><EM>R</EM> = 3</FONT>的不带验证的空着裁剪;
<DT>  <FONT face="Times New Roman">(4) </FONT>如果浅一层的搜索高出边界,那么带验证的空着裁剪是成功的,否则必须重新做完全的搜索。
<DT>  笔者认为这里存在很多问题:
<DT>  <FONT face="Times New Roman">(1) </FONT>用<FONT face="Times New Roman"><EM>R</EM> = 3</FONT>非常冒进,改用适应性空着裁剪推荐的<FONT face="Times New Roman"><EM>R</EM> = 2<SUP><U>6/8</U></SUP>3</FONT>法则比较合适;
<DT>  <FONT face="Times New Roman">(2) </FONT>检验时做浅一层的搜索太浪费时间,裁剪的层数可以跟空着裁剪一样的<FONT face="Times New Roman"><EM>R</EM></FONT>值一样,而且窗口也用以<FONT face="Times New Roman">Beta</FONT>为界的零窗口;
<DT>  <FONT face="Times New Roman">(3) </FONT>做检验时,子结点仍旧应该做带检验的空着裁剪,否则“连停着杀”就检测不出来了。
<DT> 
<DT>  <FONT face="Times New Roman">ElephantEye</FONT>是否启用空着裁剪,分三种情况讨论:
<DT>  <FONT face="Times New Roman">(1) </FONT>我方子力超过<FONT face="Times New Roman">3</FONT>个轻子的价值,就使用不带检验的适应性空着裁剪;
<DT>  <FONT face="Times New Roman">(2) </FONT>我方子力小于<FONT face="Times New Roman">3</FONT>个轻子的价值,但有进攻子力,则使用带检验的适应性空着裁剪;
<DT>  <FONT face="Times New Roman">(3) </FONT>我方只有仕<FONT face="Times New Roman">(</FONT>士<FONT face="Times New Roman">)</FONT>相<FONT face="Times New Roman">(</FONT>象<FONT face="Times New Roman">)</FONT>等守子,则禁用空着裁剪。
<DT>  因此,<FONT face="Times New Roman">ElephantEye</FONT>的代码中,和空着裁剪有关的部分如下:
<DD> 
<DD>int AlphaBeta(int Alpha, int Beta, int Depth, int NullOkay) {
<DD> ……
<DD> if (NullOkay &amp;&amp; !InCheck() &amp;&amp; HaveAttackPieces()) {
<DD>  MakeNullMove();
<DD>  Value = -AlphaBeta(-Beta, 1 - Beta, Depth - 1 - RAdapt(Depth), 0);
<DD>  UndoMakeMove();
<DD>  if (Value &gt;= Beta) {
<DD>   if (Evalue &gt; EndgameMargin) {
<DD>    // 我方子力超过3个轻子的价值,空着裁剪不带检验
<DD>    return Value; // 如果不是高出边界的,则返回Beta,下同
<DD>   } else {
<DD>    // 我方子力小于3个轻子的价值,空着裁剪要带检验
<DD><FONT color=#ff0000>    Value = AlphaBeta(Beta - 1, Beta, Depth - RAdapt(Depth), 0);</FONT>
<DD>    if (Value &gt;= Beta) {
<DD>     // 如果检验值高出边界,则检验成功,否则检验失败
<DD>     return Value;
<DD>    }
<DD>   }
<DD>  }
<DD> }
<DD> ……
<DD>}
<DT> 
<DT>  这个技术使得<FONT face="Times New Roman">ElephantEye</FONT>在残局中仍旧能启用空着裁剪,而且不会出现走错的情况,因此<FONT face="Times New Roman">ElephantEye</FONT>在几乎不带知识的情况下,残局的棋力还是非常高的。
<DT> 
<DT><FONT face=Arial size=4><STRONG>5.3 </STRONG></FONT><FONT face=楷体_GB2312 size=4><STRONG>无效路线裁剪</STRONG></FONT>
<DT> 
<DT>  “无效路线裁剪”<FONT face="Times New Roman">(Futility Pruning)</FONT>是不同于空着裁剪的另一种裁剪方法,它首先由<FONT face="Times New Roman">Heinz</FONT>发表在<FONT face="Times New Roman">1998</FONT>年的<FONT face="Times New Roman">ICCA</FONT>杂志上<FONT face="Times New Roman">(</FONT>参阅:<FONT face="Times New Roman">Heinz EA: <EM><STRONG>Extended Futility Pruning</STRONG></EM><EM>, ICCA J.</EM> 21(2): 75-83, <STRONG>1998</STRONG></FONT>,可从互联网上找到<FONT face="Times New Roman">)</FONT>,包括了<FONT face="Times New Roman">AEL</FONT>算法中的<FONT face="Times New Roman">E</FONT>和<FONT face="Times New Roman">L</FONT>两个部分,它们是基于同一个思想的:当局面相对于主要变例少许落后时,搜索深度可以裁剪一层,相当落后时,可以裁剪两层,大幅度落后时,可以裁剪三层。然而裁剪的这三个层次的手法是不同的,裁剪一层称为“选择性无效路线裁剪”<FONT face="Times New Roman">(Selective Futility Pruning)</FONT>,裁剪两层称为“延伸的无效路线裁剪”<FONT face="Times New Roman">(Extended Futility Pruning)</FONT>,裁剪三层称为“限制性修剪”<FONT face="Times New Roman">(Limited Razoring)</FONT>。
<DT>  在国际象棋中,裁剪一层、两层和三层的条件分别界定为落后一个轻子、两个轻子和三个轻子,而没有文献为中国象棋给定一个数值。《象棋世家》的作者郑明政经过大量试验,认为落后一相<FONT face="Times New Roman">(</FONT>象<FONT face="Times New Roman">)</FONT>裁剪一层,落后一马裁剪两层,落后一车裁剪三层比较适合。<FONT face="Times New Roman">ElephantEye</FONT>由于没有做试验,因此用得比较保守:
<DD> 
<DD>const int SelFutMargin = 80; // 不到一个马
<DD>const int ExtFutMargin = 160; // 不到一个车
<DD>const int RazorMargin = 240; // 超过一个车
<DT> 
<DT>  尽管裁剪一层、两层和三层在思路上是一致的,但具体操作和界定上有明显的差别,看下面的代码:
<DD> 
<DD>int AlphaBeta(int Alpha, int Beta, int Depth) {
<DD> ……
<DD> FutPrune = 0;
<DD> // a. 在与叶子结点差三层的结点上,采用限制性修剪
<DD> if (Depth == 3 &amp;&amp; !Extended &amp;&amp; Evaluation() + RazorMargin &lt;= Alpha &amp;&amp;
<DD> Evalue &gt; EndgameMargin) {
<DD>  Depth = 2;
<DD> }
<DD> // b. 在与叶子结点差两层的结点上,采用延伸的无效路线裁剪
<DD> if (Depth == 2 &amp;&amp; !Extended) {
<DD>  Value = Evaluation() + ExtFutMargin;
<DD>  if (Value &lt;= Alpha) {
<DD>   FutPrune = 1;
<DD>   BestValue = Value;
<DD>   FutMargin = ExtFutMargin;
<DD>  }
<DD> }
<DD> // c. 在与叶子结点差一层的结点上,采用选择性无效路线裁剪
<DD> if (Depth == 1 &amp;&amp; !InCheck) {
<DD>  Value = Evaluation() + SelFutMargin;
<DD>  if (Value &lt;= Alpha) {
<DD>   FutPrune = 1;
<DD>   BestValue = Value;
<DD>   FutMargin = SelFutMargin;
<DD>  }
<DD> }
<DD> // 随后逐一检查每个合理着法
<DD> ……
<DD>}
<DT> 
<DT>  在操作上可以很明显地看出,裁剪三层和两层的做法是截然不同的,裁剪三层就把即将搜索的深度减去<FONT face="Times New Roman">1</FONT>,它正好和“延伸”<FONT face="Times New Roman">(Extension)</FONT>相反,因此称为“修剪”<FONT face="Times New Roman">(Razoring)</FONT>。对于修剪或裁剪的条件,三层比两层苛刻得多,两层比一层又苛刻得多,这不仅体现在子力上,还有其他因素:裁剪一层只需要当前局面不被将军即可,而裁剪两层需要当前局面没有延伸,这自然包括了不被将军,另外还不能符合兑子或其他延伸条件;另外,裁剪三层要看对方子力有多少,如果处于残局则不适宜做修剪。
<DT>  裁剪两层和一层时,都设置<FONT face="Times New Roman">FutPrune</FONT>这个变量,表明该结点可以实施裁剪。但究竟是否能够裁剪,还需要视该结点的每个着法而定,看随后的代码:
<DD> 
<DD>int AlphaBeta(int Alpha, int Beta, int Depth) {
<DD> ……
<DD> // 先判断是否能做修剪或裁剪
<DD> GenMoves();
<DD> while(MovesLeft()) {
<DD>  MakeNextMove();
<DD>  <FONT color=#ff0000>if (FutPrune &amp;&amp; FutMargin - Evaluation() &lt;= Alpha &amp;&amp; !InCheck()) {</FONT>
<DD>   UndoMakeMove();
<DD>  } else {
<DD>   Value = -AlphaBeta(-Beta, -Alpha, Depth - 1);
<DD>   UndoMakeMove();
<DD>   ……
<DD>  }
<DD> }
<DD> ……
<DD>}
<DT> 
<DT>  我们注意到,进行裁剪要符合三个条件:<FONT face="Times New Roman">(1) </FONT>前面已经判断过该结点能做裁剪;<FONT face="Times New Roman">(2) </FONT>对手走完后,子力仍旧要符合裁剪的条件;<FONT face="Times New Roman">(3) </FONT>对手的着法不能带将军。值得注意的是,这三个条件对于限制性修剪是无效的,如果符合条件,则可以裁剪三层<FONT face="Times New Roman">(</FONT>因为满足限制性修剪条件的结点也一定设置了<FONT face="Times New Roman">FutPrune)</FONT>,否则还需要做两层搜索<FONT face="Times New Roman">(</FONT>即只裁剪了一层<FONT face="Times New Roman">)</FONT>,因此有算不到弃子杀的风险。
<DT>  最后要说明的是,无效路线裁剪和限制性修剪在<FONT face="Times New Roman">ElephantEye</FONT>中效果并不好,可能是<FONT face="Times New Roman">E</FONT>和<FONT face="Times New Roman">L</FONT>的条件设得比较保守的缘故。另外,裁剪一层的条件最宽松,尽管它使<FONT face="Times New Roman">ElephantEye</FONT>搜索的结点数减少了,但速度并未提高,因为被裁剪结点的每个着法还是走了<FONT face="Times New Roman">(ElephantEye</FONT>在运行过程中,绝大部分时间花在生成和执行着法上<FONT face="Times New Roman">)</FONT>,因此裁剪掉的部分只是叶子结点上静态搜索的部分。
<DT> 
<DT><FONT face=Arial size=4><STRONG>5.4 </STRONG></FONT><FONT face=楷体_GB2312 size=4><STRONG>其他可以考虑的裁剪方法</STRONG></FONT>
<DT> 
<DT>  裁剪算法并非只有上面所说的几种,如果程序设计师思路开阔,就可以想出很多裁剪策略。例如根据无效路线裁剪的“裁剪一层、两层和三层”的思路,笔者设计出一套形式上和空着裁剪类似的方案:
<DD> 
<DD>int AlphaBeta(int Alpha, int Beta, int Depth) {
<DD> ……
<DD> // 前面是尝试空着裁剪
<DD> // a. 尝试裁剪三层,类似限制性修剪,用RazorMargin
<DD> Value = AlphaBeta(Beta + RazorMargin - 1, Beta + RazorMargin, Depth - 3);
<DD> if (Value &gt;= Beta + RazorMargin) {
<DD>  return Value; // 如果不是高出边界的,则返回Beta,下同
<DD> }
<DD> // b. 尝试裁剪两层,类似延伸的无效线路裁剪,用ExtFutMargin
<DD> Value = AlphaBeta(Beta + ExtFutMargin - 1, Beta + ExtFutMargin, Depth - 2);
<DD> if (Value &gt;= Beta + ExtFutMargin) {
<DD>  return Value;
<DD> }
<DD> // c. 尝试裁剪一层,类似选择性无效线路裁剪,用SelFutMargin
<DD> Value = AlphaBeta(Beta + SelFutMargin - 1, Beta + SelFutMargin, Depth - 1);
<DD> if (Value &gt;= Beta + SelFutMargin) {
<DD>  return Value;
<DD> }
<DD> // 后面是搜索全部着法
<DD> ……
<DD>}
<DT> 
<DT>  <FONT face="Times New Roman">ElephantEye</FONT>没有使用这套方案。裁剪方案是否有效,除了要通过大量测试来证明外,还要在实战中检测效果。因此任何裁剪方案若要上升到理论的高度,都不是很简单的。
<DT> 
<DT><FONT face=Arial size=4><STRONG>5.5 </STRONG></FONT><FONT face=楷体_GB2312 size=4><STRONG>静态搜索</STRONG></FONT>
<DT> 
<DT>  静态搜索尽管实现起来比较简单,但是很多技术细节仍旧是需要考虑的,问题主要有以下几个:
<DT>  <FONT face="Times New Roman">(1) </FONT>如果结点被将军,是否要产生全部着法。绝大多数程序都没有考虑结点被将军的情况,因为将军判断是很花费时间的。而<FONT face="Times New Roman">ElephantEye</FONT>在将军判断上有优势,因此在将军时产生全部的着法。因此有些多步连杀的排局,如果每步将军都吃子,那么<FONT face="Times New Roman">ElephantEye</FONT>只需要搜索<FONT face="Times New Roman">1</FONT>层就可以解出了,因此静态搜索中判断将军对于搜索杀棋是很有好处的。
<DT>  <FONT face="Times New Roman">(2) </FONT>如何使用<FONT face="Times New Roman">MVV/LVA</FONT>启发。<FONT face="Times New Roman">MVV/LVA</FONT>是静态搜索最常用的启发方式,如果棋盘的数据结构精心设计,<FONT face="Times New Roman">SEE</FONT>也是值得尝试的。尽管<FONT face="Times New Roman">MVV/LVA</FONT>很简单,但是究竟根据“被吃子价值<FONT face="Times New Roman">-</FONT>攻击子价值”来排序,还是先排序“被吃子价值”,相同的情况再排序“攻击子价值”呢?这两种策略都很流行,<FONT face="Times New Roman">ElephantEye</FONT>使用后者。另外,<FONT face="Times New Roman">MVV/LVA</FONT>的子力价值越简单越好,<FONT face="Times New Roman">ElephantEye</FONT>已经简化到帅<FONT face="Times New Roman">(</FONT>将<FONT face="Times New Roman">)4</FONT>分,车<FONT face="Times New Roman">3</FONT>分,马炮<FONT face="Times New Roman">2</FONT>分,仕<FONT face="Times New Roman">(</FONT>士<FONT face="Times New Roman">)</FONT>相<FONT face="Times New Roman">(</FONT>象<FONT face="Times New Roman">)</FONT>兵<FONT face="Times New Roman">(</FONT>卒<FONT face="Times New Roman">)1</FONT>分的程度了。
<DT>  <FONT face="Times New Roman">(3) </FONT>是否需要用其他搜索或启发方法。常规搜索中的很多搜索算法都不适合于静态搜索,这就是静态搜索简单的缘故。静态搜索本身就是空着启发的<FONT face="Times New Roman">(</FONT>第一个着法总是空着<FONT face="Times New Roman">)</FONT>,但由于没有深度参数,所以不可能使用空着裁剪。另外,几乎所有的程序都不在静态搜索中使用置换表、<FONT face="Times New Roman">PVS</FONT>、杀手启发等算法。
<DT>  <FONT face="Times New Roman">(4) </FONT>是否所有的吃子都需要考虑。如果搜索全部吃子着法,那么程序在叶子结点上花费的时间就非常浪费。<FONT face="Times New Roman">ElephantEye</FONT>在搜索所有吃子着法时,对着法作了过滤——吃不过河的兵和吃仕<FONT face="Times New Roman">(</FONT>士<FONT face="Times New Roman">)</FONT>相<FONT face="Times New Roman">(</FONT>象<FONT face="Times New Roman">)</FONT>的着法都不搜索了,尽管仕<FONT face="Times New Roman">(</FONT>士<FONT face="Times New Roman">)</FONT>和相<FONT face="Times New Roman">(</FONT>象<FONT face="Times New Roman">)</FONT>在<FONT face="Times New Roman">ElephantEye</FONT>的子力评价中分数很高<FONT face="Times New Roman">(</FONT>轻子价值的<FONT face="Times New Roman">40%)</FONT>,但静态搜索本身就是对局面的粗略评价。
<DT> 
<DT><FONT face=Arial size=4><STRONG>5.6 </STRONG></FONT><FONT face=楷体_GB2312 size=4><STRONG>选择性延伸</STRONG></FONT>
<DT> 
<DT>  常用的选择性延伸有这几种策略:<FONT face="Times New Roman">(1) </FONT>将军延伸;<FONT face="Times New Roman">(2) </FONT>单一应着延伸;<FONT face="Times New Roman">(3) </FONT>杀棋威胁延伸;<FONT face="Times New Roman">(4) </FONT>兑子延伸;<FONT face="Times New Roman">(5) </FONT>通路兵挺进延伸。<FONT face="Times New Roman">ElephantEye</FONT>只考虑了将军延伸和兑子延伸,代码很简单:
<DD> 
<DD>int AlphaBeta(int Alpha, int Beta, int Depth) {
<DD> ……
<DD> Extended = 0;
<DD> if (InCheck()) {
<DD>  Depth ++;
<DD>  Extended = 1;
<DD> } // 将军延伸
<DD> if (!Extended &amp;&amp; LastMove().IsGoodCap() &amp;&amp; LastSecondMove().IsGoodCap()) {
<DD>  Depth ++;
<DD>  Extended = 1;
<DD> } // 兑子延伸
<DD> ……
<DD>}
<DT> 
<DT>  这里的兑子延伸策略是:遇到连续两个吃子着法就延伸一层,连续三个吃子着法就延伸两层,等等。这样,车换马炮的兑子就延伸了两层,而两次连续的简单兑子则延伸了三层,当然,这样做也并非不合理。
<DT>  <FONT face="Times New Roman">ElephantEye</FONT>之所以没有做杀棋威胁延伸,是因为在连杀排局中每着都有杀棋威胁,延伸层数太多会导致杀棋的求解速度慢很多。其代码实现起来并不困难,关键是利用空着裁剪:
<DD> 
<DD>int AlphaBeta(int Alpha, int Beta, int Depth) {
<DD> ……
<DD> MakeNullMove();
<DD> Value = -AlphaBeta(-Beta, 1 - Beta, Depth - 3);
<DD> UndoMakeMove();
<DD> if (Value &gt;= Beta) {
<DD>  ……
<DD> }
<DD><FONT color=#ff0000> if (!Extended &amp;&amp; Value &lt; MaxMoveNum - MaxValue) {</FONT>
<DD><FONT color=#ff0000>  Depth ++;</FONT>
<DD><FONT color=#ff0000>  Extended = 1;</FONT>
<DD><FONT color=#ff0000> }</FONT>
<DD> ……
<DD>}
<DT> 
<DT>  做空着裁剪时,如果返回一个被杀的分值,则说明面临杀棋威胁,因此需要作延伸。如果程序没有将军判断的功能,只在帅<FONT face="Times New Roman">(</FONT>将<FONT face="Times New Roman">)</FONT>被吃时才返回被杀的分值,那么做空着裁剪就能发现被将军的局面,因此杀棋威胁延伸是包括将军延伸的。
<DT>  为避免裁剪带来的负面作用,选择性延伸是的最有效的手段。在没有延伸的情况下,<FONT face="Times New Roman"><EM>n</EM></FONT>回合以后的局面需要<FONT face="Times New Roman">2<EM>n</EM></FONT>层搜索才能看到,而采用了将军延伸以后,<FONT face="Times New Roman"><EM>n</EM></FONT>回合的连杀就可以在<FONT face="Times New Roman"><EM>n</EM></FONT>回合内看到了。同样,如果兑子延伸能识别弃车破仕<FONT face="Times New Roman">(</FONT>士<FONT face="Times New Roman">)</FONT>、一马换双相<FONT face="Times New Roman">(</FONT>象<FONT face="Times New Roman">)</FONT>等手段,那么当这些路线因为子力落后而被裁剪的同时,也会因兑子而延伸,从而抵消了裁剪带来的负面作用。 </DT></DL></DL></DL>

页: [1]
查看完整版本: 解剖大象的眼睛——中国象棋程序设计探索(五):消除水平线效应