既然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