前言
之前毕设做了一个象棋游戏,做个记录。
极大极小值搜索算法
什么是极大极小搜索算法?
所有双向搜索算法的最基本的思想都是“极大-极小”(MAX-MIN)原理。它可以追溯到中世纪,但最早是由冯-诺依曼(John von Nuoma,1903-1957,美籍匈牙利数学家)在60年前完整描述的[6]:
- 假设有对局面评分的方法,来预测棋手甲(极大者MAX)会赢,或者对手(极小者MIN)会赢,或者是和棋。评分用数字表示,正数代表极大者领先,负数代表极小者领先,零代表谁也不占便宜。
- 极大者的任务是增加棋盘局面的评分(即尽量让评分最大)。
- 极小者的任务是减少棋盘局面的评分(即尽量让评分最小)。
- 假设谁也不会犯错误,即他们都走能让使局面对自己最有利的走法。
在AI算法中,电脑就是极大者,极大者和极小者的多次任务的交替就形成了一颗博弈树,每次任务就是一层树节点,这层树节点的每个节点就是当前任务的一种走法选择情况。在确定了博弈树搜索深度即考虑的走棋步数的情况下,算法从下而上回溯,到根节点就可以确定一个当前算法环境下的最优解,即可以确定电脑的走棋。
如上图中,对于max来说,它的最优路径是我w1-w2-w4,如果他选择w3,那么min肯定会选择w7
博弈树中每个节点都对应一个棋局局面的评分,而评分则是根据自定义的局面评估函数确定的,不同的评估函数也会使电脑最终的走棋选择不同。局面评估函数要考虑的因素有但不限于以下几点:
- 棋子战斗力:不同的棋子所拥有的战斗力是不同的,如“車”的战斗力一般大于“卒”的战斗力,但如果“卒”过河了,卒的战斗力就会发生改变。评估函数中所考虑的棋子战斗力是当前棋局局面上两方各自所有存活的棋子的战斗力的总和的差值。用RedScore来表示所有存活的红方棋子的战斗力总和,用BlackScore来表示所有存活的黑方棋子的战斗力总和,当BlackScore > RedScore时,表明当前棋局局势黑方优于红方,差值越大表示优势越大。
- 棋子灵活性:不同的棋子所拥有的灵活性是不同的,如“炮”的灵活性明显优于“卒”的,即使“卒”过河后战斗力得到了很大的提升,但灵活性依然较低。评估函数中考虑棋子灵活性的方式和考虑棋子战斗力的方式类似。
- 棋子相互关系:不同的棋子之间的组合,会使得棋子的战斗力得到提升,如连环炮或连环马等。
如果纯粹使用极大-极小算法来搜索棋局最优解,那么形成的博弈树节点会随着所考虑的下棋步数的增加成指数形式增加,所以为了减少树节点而引入Alpha-Beta剪枝,就形成了Alpha-Beta搜索算法。平均而言,在同样资源限制下,Alpha-Beta剪枝算法要比原始MAX-MIN算法搜索的树深度多一倍,也就是说,可以比MAX-MIN向前看的步数多一倍。
Alpha-Beta剪枝算法
Alpha-Beta剪枝的设计思路是引入两个界限参数。Alpha表示下限值,即一个节点所能到达的最小局面评估值;Beta表示上限值,即一个节点所能到达的最大局面评估值[7]。对于一个MIN节点来说,它会尽可能的使得自己的Beta值更小;对于一个MAX节点来说,它会尽可能使得自己的Alpha值更大,以下是剪枝过程描述:
- 对于一个MIN节点,若能估计出其倒推值的上限界Beta,并且这个Beta值不大于MIN的父节点(MAX节点)的估计倒推值的下限界Alpha,即Alpha≥Beta,则就不必再扩展该MIN节点的其余子节点了,因为这些节点的估值对MIN父节点的倒推值已无任何影响了。这一过程称为Alpha剪枝。
- 对于一个MAX节点,若能估计出其倒推值的下限界Alpha,并且这个Alpha值不小于MAX的父节点(MIN节点)的估计倒推值的上限界Beta,即Alpha≥Beta,则就不必再扩展该MAX节点的其余子节点了,因为这些节点的估值对MAX父节点的倒推值已无任何影响了。这一过程称为Beta剪枝。
- 一个MAX节点的Alpha值等于其后继节点当前最大的最终倒推值,一个MIN节点的Beta值等于其后继节点当前最小的最终倒推值。
- 剪枝开始时根节点的Alpha设置为-MAX_VALUE,Beta设置为MAX_VALUE。
/**
*
* @param depth 深度
* @param palyer 为true代表电脑为max方;false代表玩家为min方
* @param alpha 下界
* @param beta 上界
* @return
*/
private int alphaBetaSearch2(int depth,Boolean palyer, int alpha, int beta) {
if (depth == 0) {//走完设置的深度,返回此时的局面评估值
return AIUtil.evaluate(tempBoard.getPlayerName(),
tempChessPoints, board.getPalyerFirst());
}
/*得到当前走棋方所有可能的走棋情况*/
ArrayList<ManualItem> chessMoves = AIUtil.generateAllChessMove(board
.getPlayerName(), chessPoints, board.getPalyerFirst());
int size = chessMoves.size();
if (palyer) {//max
for (int index = 0; index < size; index++) {
ManualItem chessMove = chessMoves.get(index);
makeMove(chessMove);//走棋
//递归,得到子节点的beta返回值
int value = alphaBetaSearch2(depth - 1,!palyer,alpha, beta);
unMakeMove(chessMove);//恢复上一步的棋局局面,以便进行另一种走棋方式
if(value > alpha){
//通过向上传递的子节点min的beta值修正alpha值,
// 注意这里的value是字节点min的beta,而下面的beta是父节点min的
alpha = value;//更新max节点所能达到的局面下限值(max会尽量使得自己的下限值更大)
bestChessMove = chessMove;//记录最好走法
if(alpha >= beta) { //这里的beta是max的父节点min传下来的
/*如果父节点min的局面评估上限值beta不大于其子节点max的局面评估下限值alpha,
则说明父节点max根本不会选择走到该子节点max
因为对于父节点min来说,老子自己已经出现的beta上限值不大于儿子的下限值,
那就说明在回溯的时候儿子并不会让老子的上限值减小,
所以这条路径上之后的节点也就没有意义了,就把这条分支给剪了
记忆:回溯时不会使得父亲节点min的上限值beta值减小而剪枝,叫做beta剪枝* */
return alpha;//一个MIN节点的Beta值等于其后继节点当前最小的最终倒推值
}
}
}
return alpha;
}
else{//min
for (int index = 0; index < size; index++) {
ManualItem chessMove = chessMoves.get(index);
makeMove(chessMove);
//递归,得到子节点的alpha返回值
int value = alphaBetaSearch2(depth - 1,!palyer, alpha, beta);
unMakeMove(chessMove);
if(value < beta){
//通过向上传递的子节点max的alpha值修正beta值,
// 注意这里的value是字节点max的alpha,而下面的alpha是父节点max的
beta = value;
//更新min节点所能达到的局面评估上限值(min会尽量使得自己的上限值更小)
bestChessMove = chessMove;//记录最好走法
System.out.println("minSearch找到了最佳走法!787");
if(alpha >= beta) {//这里的alpha是min的父节点max传下来的
/*如果父节点max的局面评估下限值alpha不小于其子节点min的局面评估上限值,
则说明父节点max根本不会选择走到该子节点min,
因为对于父节点max来说,老子自己已经出现的alpha下限值不小于儿子的上限值,
那就说明在回溯的时候儿子并不会让老子的下限值增大,
所以这条路径上之后的节点也就没有意义了,就把这条分支给剪了
记忆:回溯时不会使得父亲节点max的下限值alpha值增大而剪枝,叫做alpha剪枝*/
return beta;//一个MAX节点的Alpha值等于其后继节点当前最大的最终倒推值,回溯
}
}
}
return beta;
}