既然Alpha-Beta搜索算法是在“最大-最小”的基础上引入“树的裁剪”的思想以期提高效率,那么它的效率就取决于树的结构——如果搜索了没多久就发现可以“裁剪”了,那么需要分析的工作量将大大减少,效率自然也就大大提高;而如果直到分析了所有的可能性之后才能作出“裁剪”判断,那此时“裁剪”也已经失去它原有的价值(因为你已经分析了所有情况)。因而,要想保证Alpha-Beta搜索算法的效率就需要调整树的结构,即调整待搜索的结点的顺序,保证“裁剪”可以尽可能早地发生。

我们可以根据部分已经搜索的结果来调整将要搜索的结点的顺序。因为通常一个局面经搜索被认为较好时,在其后继结点中往往有一些相似的局面(如某些无关紧要的棋子位置有所不同)也是较好的。“历史启发”就是建立在这样一种观点之上的。在搜索的过程中,每当找到一个好的走法,我们就给该走法累加一个增量以记录其“历史得分”,一个多次被搜索并认为是好的走法的“历史得分”就会较高。对于即将搜索的结点,按 “历史得分”的高低对它们进行排序,保证较好的走法(“历史得分”高的走法)排在前面,这样Alpha-Beta搜索就可以尽可能早地进行“裁剪”,从而保证了搜索的效率。

下面分别是历史启发部分HistoryHeuristic.h以及着法排序部分SortMove.h的代码实现,着法排序可以使用各种排序算法,这里我直接借用了王小春的《PC 游戏编程(人机博弈)》中采用的“归并排序”。

历史启发:

// HistoryHeuristic.h

#include     // For  void *memset( void *dest, int c, size_t count );

/////////////////// Data Define ///////////////////////////////////////////////

int HistoryTable[90][90];      //历史记录表

/////////////////// Function Prototype ////////////////////////////////////////

// 清空历史记录表(全置0)
inline void ResetHistoryTable();

// 取给定走法(move)的历史得分,返回该得分
inline int GetHistoryScore( CCHESSMOVE *move );

// 为一最佳走法(move)增添历史记录得分,nDepth标志该走法所属的搜索层数
inline void EnterHistoryScore( CCHESSMOVE *move, int nDepth );

////////////////// Programmer-Defined Function ////////////////////////////////

inline void ResetHistoryTable()
{
  memset( HistoryTable, 0, sizeof(int)*8100 );
}

inline int GetHistoryScore( CCHESSMOVE *move )
{
  int nFrom, nTo ;

  // 将位置坐标转换为数组下标
  nFrom  = move->ptFrom.x * 9 + move->ptFrom.y;
  nTo    = move->ptTo.x   * 9 + move->ptTo.y;

  return HistoryTable[nFrom][nTo];  // 返回历史纪录表中的分数
}

inline void EnterHistoryScore( CCHESSMOVE *move, int nDepth )
{
  int nFrom, nTo;

  // 将位置坐标转换为数组下标
  nFrom  = move->ptFrom.x * 9 + move->ptFrom.y;
  nTo    = move->ptTo.x   * 9 + move->ptTo.y;

  HistoryTable[nFrom][nTo] += 2 << nDepth;  // 增量为2的nDepth次方
}

// end of HistoryHeuristic.h

着法排序:

// SortMove.h

/////////////////// Data Define ///////////////////////////////////////////////

CCHESSMOVE cmTargetBuffer[80];    //排序用的缓冲队列

/////////////////// Function Prototype ////////////////////////////////////////

// 对长度为nCount的着法队列cmSource进行归并排序
void MergeSort( CCHESSMOVE *cmSource, int nCount );    

// 为MergeSort函数所调用,归并排序首尾相接的两个数组,
void MergePass( CCHESSMOVE *cmSource, CCHESSMOVE *cmTarget,
 int nLength, int nCount  ); 

// 为MergePass函数所调用,归并排序首尾相接的两个数组(这两个数组已分别排好序)
//!!!降序 !!!
// 两个数组为cmSource[beginOne]--cmSource[endOne];
//cmSource[beginTwo]--cmSource[endTwo]. 其中beginTwo = endOne + 1
inline void Merge_Desc( CCHESSMOVE *cmSource, CCHESSMOVE *cmTarget,
 int beginOne ,int endOne, int endTwo );

////////////////// Programmer-Defined Function ////////////////////////////////

void MergeSort( CCHESSMOVE *cmSource, int nCount )
{
  int nLength = 1;

  // 借助cmTargetBuffer对cmSource进行归并排序
  while( nLength < nCount )
  {
    MergePass( cmSource , cmTargetBuffer , nLength , nCount );
    nLength += nLength ;
    MergePass( cmTargetBuffer , cmSource , nLength , nCount );
    nLength += nLength ;
  }
}

void MergePass( CCHESSMOVE *cmSource, CCHESSMOVE *cmTarget,
 int nLength, int nCount  )
{
  int i = 0;  // 表记待排序的第一个数组的起点

  while( i + 2 * nLength <= nCount ) // 剩余的元素个数大于等于两个nLength长度
  {
    // 归并排序长度为 nLength 的首尾相接的两个数组
    Merge_Desc( cmSource, cmTarget, i, i + nLength - 1, i + 2 * nLength - 1 );

    i += 2 * nLength ;
  }

  if( i + nLength < nCount )  //剩余的元素个数小于两个nLength长度,
  {                                      //但大于一个nLength长度
    // 归并排序长度为 nLength 的数组和与其相接的剩下的所有元素组成的数组
    Merge_Desc( cmSource, cmTarget, i, i + nLength - 1, nCount - 1 );
  }
  else            // 剩余的元素个数小于等于一个nLength长度
  {
    int j;
    // 接上剩下的已经排序好的数组
    for ( j = i ; j < nCount ; j ++ )
      cmTarget[j] = cmSource[j];
  }

}

inline void Merge_Desc( CCHESSMOVE *cmSource, CCHESSMOVE *cmTarget,
 int beginOne ,int endOne, int endTwo )
{
  int i = beginOne ,
    j = endOne + 1,// j = beginTwo
    k = beginOne ;

  while( i <= endOne  &&  j <= endTwo )  //取两个数组中值大的一个放入cmTarget
  {                                                       //(降序)
    if( cmSource[i].nScore >= cmSource[j].nScore )
      cmTarget[k++] = cmSource[i++];
    else
      cmTarget[k++] = cmSource[j++];
  }

  if( i <= endOne )  // 第一个数组尚未完,则接上第一个数组的剩余部分
  {
    int q;
    for( q = i; q <= endOne; q ++ )
      cmTarget[k++] = cmSource[q];
  }
  else // j <= endTwo    第二个数组尚未完,则接上第二个数组的剩余部分
  {
    int q;
    for( q = j; q <= endTwo; q ++ )
      cmTarget[k++] = cmSource[q];
  }
}

// end of SortMove.h