alpha-beta剪枝算法是基于极大极小搜索算法的。极大极小搜索策略是考虑双方对弈若干步之后,从可能的步中选一步相对好的走法来走,在有限的搜索范围内进行求解,可以理解为规定一个有限的搜索深度。
为此要定义一个静态估计函数f,以便对棋局的势态做出优劣的估计,这个函数可根据棋局的优劣势态的特征来定义。这里规定,MAX代表程序方,MIN代表对手方,P代表一个棋局(即一个状态)。有利于MAX的势态,f(p)取正值,有利于MIN的势态,f(p)去负值,势态均衡,f(p)取零。极大极小搜索的基本思想是:
(1)当轮到MIN走步的节点时,MAX应考虑最坏的情况(因此,f(p)取极小值)。
(2)当轮到MAX走步的节点时,MAX应考虑最好的情况(因此,f(p)取极大值)。
(3)当评价往回倒退的时候,相应于两位棋手的对抗策略,不同层上交替地使用(1)、(2)两种方法向上传递倒推值。所以这种搜索方法称为极大极小过程。实际上,这种算法是假定在模拟过程中双方都走出最好的一步,对MAX方来说,MIN方的最好一步是最坏的情况,MAX在不断地最大化自己的利益。
极大极小搜索策略在一些棋盘AI中非常常见,但是它有个致命的弱点,就是非常暴力地搜索导致效率不高,特别是当讲搜索的深度加大时会有明显的延迟,alpha-beta在此基础上进行了优化。事实上,MIN、MAX不断的倒推过程中是存在着联系的,当它们满足某种关系时后续的搜索是多余的!alpha-beta剪枝算法把生成后继和倒推值估计结合起来,及时减掉一些无用分支,以此来提高算法的效率。
定义极大层的下界为alpha,极小层的上界为beta,alpha-beta剪枝规则描述如下:
(1)alpha剪枝。若任一极小值层结点的beta值不大于它任一前驱极大值层结点的alpha值,即alpha(前驱层) >= beta(后继层),则可终止该极小值层中这个MIN结点以下的搜索过程。这个MIN结点最终的倒推值就确定为这个beta值。
(2)beta剪枝。若任一极大值层结点的alpha值不小于它任一前驱极小值层结点的beta值,即alpha(后继层) >= beta(前驱层),则可以终止该极大值层中这个MAX结点以下的搜索过程,这个MAX结点最终倒推值就确定为这个alpha值。
先看极大极小搜索算法:
//node记录当前player,depth记录搜索深度
function minimax(node, depth)
// 如果能得到确定的结果或者深度为零,使用评估函数返回局面得分
if node is a terminal node or depth = 0
return the heuristic value of node
// 如果轮到对手走棋,是极小节点,选择一个得分最小的走法
if the adversary is to play at node
let α := +∞
for each child of node
α := min(α, minimax(child, depth-1))
// 如果轮到我们走棋,是极大节点,选择一个得分最大的走法
else {we are to play at node}
let α := -∞
foreach child of node
α := max(α, minimax(child, depth-1))
return α;
alpha-beta剪枝就在极大极小搜索算法上优化:
function alphabeta(node, depth, α, β, Player)
//达到最深搜索深度或胜负已分
if depth = 0 or node is a terminal node
return the heuristic value of node
if Player = MaxPlayer // 极大节点
for each child of node // 子节点是极小节点
α := max(α, alphabeta(child, depth-1, α, β, not(Player) ))
if β ≤ α
// 该极大节点的值>=α>=β,该极大节点后面的搜索到的值肯定会大于β,因此不会被其上层的极小节点所选用了。对于根节点,β为正无穷
break //beta剪枝
return α
else // 极小节点
for each child of node //子节点是极大节点
β := min(β, alphabeta(child, depth-1, α, β, not(Player) )) // 极小节点
if β ≤ α // 该极大节点的值<=β<=α,该极小节点后面的搜索到的值肯定会小于α,因此不会被其上层的极大节点所选用了。对于根节点,α为负无穷
break //alpha剪枝
return β
可以看到alpha-beta剪枝每次跟踪两个变量alpha和beta,对于MAX方,beta是父节点MIN的一个上界,当前搜索到alpha父节点的上界时,没有必要继续搜索了,因为已经达到了父节点的上界;对于MIN方,alpha是父节点MAX的一个下界,当前搜索到alpha父节点的下界时,没有必要继续搜索了,因为已经达到了父节点的下界,最搜索下去只是徒劳。
alpha-beta剪枝算法仅仅是AI的核心,做一个中国象棋AI还涉及到其他很多的方面的准备。其中最为重要的就是静态局面评估函数,它决定了象棋AI的聪明程度,通常来说是根据人的经验来进行的评估的,不同的人有不同的版本。此外还有涉及到中国象棋的诸多规则,因此细节方面的事情不少。
struct states{//一个走棋动作
pair<int,int>from;//源头
pair<int,int>to;//目的
bool player;
int source;//源头棋子
int target;//目的棋子
states(){
from = to = pair<int,int>(0,0);
player = false;
source = target = 0;
}
states(int fx,int fy,int tx,int ty,int tar){
from = pair<int,int>(fx,fy);
to = pair<int,int>(tx,ty);
target = tar;
player = source = 0;
}
};
struct ChessInfo{//存储每个中国象棋状态
pair<int,int>index;//位置
int value;
bool alive;
int type;
};
class ChessBoard{
private:
int fx,fy,tx,ty;//移棋的源和目标
bool player;//当前玩家在哪一方
bool isBegin;
stack chessRecord;//移棋记录,用于悔棋
pair<int,int>redGen;//红将位置
pair<int,int>blackGen;//黑将位置
std::map<int,ChessInfo>chesses;//32 chesses
int search_depth;
states aiAction;
public:
ChessBoard();
~ChessBoard();
void AiTestFunc();
bool CouldMove(int record[10][9],int fy,int fx,int ty,int tx);//合法走棋判断
//could move or not
bool CouldGeneral(int record[10][9],int &fy,int &fx,int &ty,int &tx);//帅 or 将
bool CouldChariot(int record[10][9],int &fy,int &fx,int &ty,int &tx);//车
bool CouldHorse(int record[10][9],int &fy,int &fx,int &ty,int &tx);//马
bool CouldCannon(int record[10][9],int &fy,int &fx,int &ty,int &tx);//炮
bool CouldAdvisor(int record[10][9],int &fy,int &fx,int &ty,int &tx);//士
bool CouldMinister(int record[10][9],int &fy,int &fx,int &ty,int &tx);//相、象
bool CouldSoldider(int record[10][9],int &fy,int &fx,int &ty,int &tx);//兵卒
bool WinJudgement(int record[10][9],pair<int,int>redGeneral,pair<int,int>blackGeneral);//胜负判断
//Ai part
int ValueEstimation(map<int,ChessInfo>& target,bool red);//静态局面评估
//AI核心算法
int AlphaBeta(int record[10][9],map<int,ChessInfo>target,int depth,int alpha,int beta);
//获取所有可能的走法
bool MakeNextMove(int record[10][9],vector & moves,ChessInfo &obj,bool red);
//悔棋,用于回溯
bool UnMakeMove(int record[10][9],map<int,ChessInfo>& target,states& move);
//移动一步
void MoveIt(int record[10][9],map<int,ChessInfo>& target,states &obj);
};
int ChessBoard::AlphaBeta(int record[10][9],map<int, ChessInfo> target,
int depth, int alpha, int beta)
{
if(!depth){//到达搜索深度
if(WinJudgement(record,target[17].index,target[1].index)){//胜负已出
cout << "win!" << endl;
return MATE;
}
else return ValueEstimation(target,!search_depth%2);//返回局面评估
}
int value = 0;
int x = (depth%2)?1:17;//根据深度判断当前玩家
int up = x + 16;
for(;x < up;x ++){//对于当前方的每一个棋子,共16个棋子
if(!target[x].alive)continue;//若棋子已死,跳过
vector moves;//记录当前棋子的走法
MakeNextMove(record,moves,target[x],x>=17);//获取当前棋子的所有走法
if(moves.empty())continue;//若为空跳过
for(int y = 0;y < moves.size();y ++){//对于每一步走法
MoveIt(record,target,moves[y]);//走这步棋子
value = -AlphaBeta(record,target,depth-1,-beta,-alpha);//递归调用,获取这步走法的局面评估
UnMakeMove(record,target,moves[y]);//回溯这步棋子
if(search_depth == depth && value > alpha){
aiAction = moves[y];//若此时时最顶层,则记录最佳走法,贪心策略
}
alpha = (value > alpha)?value:alpha;//极大搜索
if(alpha >= beta)return alpha;//剪枝
}
}
return alpha;
}
评估用简单的棋力相加(摊手0.0)
int ChessBoard::ValueEstimation(map<int,ChessInfo>& target,bool red)
{
int x = (red)?17:1;
int up = x + 16;
int ret = 0;
for(;x < up;x ++){
if(target[x].alive){
int index = (red)?(INDEX(target[x].index.first,CHESSBOARD_ROW-1-target[x].index.second)):
(INDEX(target[x].index.first,target[x].index.second));
ret += CHESSMAN_VALUE[target[x].type-1][index];
}
}
return ret;
}
最终做出来,还是感觉AI水平太弱了(逃),评估函数不行,中国象棋千变万化,高水平AI哪有那么容易。
参考资料:《人工智能基础教程(第二版》作者:朱福喜