实验二:利用α-β搜索过程的博弈树搜索算法编写一字棋游戏
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
实验二:利用α-β搜索过程的博弈树搜索算法编写一字棋游戏(3学时)
一、实验目的与要求
(1)了解极大极小算法的原理和使用方法,并学会用α-β剪枝来提高算法的效率。
(2)使用C语言平台,编写一个智能井字棋游戏。
(3)结合极大极小算法的使用方法和α-β剪枝,让机器与人对弈时不但有智能的特征,而且计算的效率也比较高。
二、实验原理
一字棋游戏是一个流传已久的传统游戏。
游戏由两个人轮流来下,分别用“X”和“O”来代替自身的棋子。
棋盘分9个格,双方可以在轮到自己下的时候,可以用棋子占领其中一个空的格子。
如果双方中有一方的棋子可以连成一条直线,则这一方判胜,对方判负。
当所有的格子都被占领,但双方都无法使棋子连成一条直线的话,则判和棋。
这是一个智能型的一字棋游戏,机器可以模拟人与用户对弈。
当轮到机器来下的时候,机器会根据当前棋局的形势,利用极大极小算法算出一个评价值,判断如何下才对自身最有利,同时也是对方来说对不利的,然后下在评价值最高的地方。
另外利用α-β剪枝,使机器在搜索评价值的时候不用扩展不必要的结点,从而提高机器计算的效率。
在用户界面方法,用一个3×3的井字格来显示用户与机器下的结果。
当要求用户输入数据的时候会有提示信息。
用户在下的过程中可以中途按下“0”退出。
当用户与计算机分出了胜负后,机器会显示出比赛的结果,并按任意键退出。
如果用户在下棋的过程中,输入的是非法字符,机器不会做出反应。
三、实验步骤和过程
1.α-β搜索过程
在极小极大搜索方法中,由于要先生成指定深度以内的所有节点,其节点数将随着搜索深度的增加承指数增长。
这极大地限制了极小极大搜索方法的使用。
能否在搜索深度不变的情况下,利用已有的搜索信息减少生成的节点数呢?设某博弈问题如下图所示,应用极小极大方法进行搜索
MINIMAX过程是把搜索树的生成和格局估值这两个过程分开来进行,即先生成全部搜索树,然后再进行端节点静态估值和倒推值计算,这显然会导致低效率。
如图1中,其中一个MIN节点要全部生成A、B、C、D四个节点,然后还要逐个计算其静态估值,最后在求倒推值阶段,才赋
给这个MIN节点的倒推值-∞。
其实,如果生成节点A后,马上进行静态估值,得知f(A)=-∞之后,就可以断定再生成其余节点及进行静态计算是多余的,可以马上对MIN节点赋倒推值-∞,而丝毫不会影响MAX的最好优先走步的选择。
这是一种极端的情况,实际上把生成和倒推估值结合起来进行,再根据一定的条件判定,有可能尽早修剪掉一些无用的分枝,同样可获得类似的效果,这就是α-β过程的基本思想。
2.利用α-β搜索过程的一字棋的实例
图2一字棋第一阶段α-β剪枝方法
为了使生成和估值过程紧密结合,采用有界深度优先策略进行搜索,这样当生成达到规定深度的节点时,就立即计算其静态估值函数,而一旦某个非端节点有条件确定其倒推值时就立即计算赋值。
从图2中标记的节点生成顺序号(也表示节点编号)看出,生成并计算完第6个节点后,第1个节点倒推值完全确定,可立即赋给倒推值-1。
这时对初始节点来说,虽然其他子节点尚未生成,但由于s属极大值层,可以推断其倒推值不会小于-1,我们称极大值层的这个下界值为α,即可以确定s的α=-1。
这说明s实际的倒推值决不会比-1更小,还取决于其他后继节点的倒推值,因此继续生成搜索树。
当第8个节点生成出来并计算得静态估值为-1后,就可以断定第7个节点的倒推值不可能大于-1,我们称极小值层的这个上界值为β,即可确定节点7的β=-1。
有了极小值层的β值,很容易发现若α≥β时,节点7的其他子节点不必再生成,这不影响高一层极大值的选取,因s的极大值不可能比这个β值还小,再生成无疑是多余的,因此可以进行剪枝。
这样一来,只要在搜索过程记住倒推值的上下界并进行比较,就可以实现修剪操作,称这种操作为α剪枝。
类似的还有β剪枝,统称为α-β剪枝技术。
在实际修剪过程中,α、β还可以随时修正,但极大值层的倒推值下界α永不下降,实际的倒推值取其后继节点最终确定的倒推值中最大的一个倒推值。
而极小值层的倒推值上界β永不上升,其倒推值则取后继节点最终确定的倒推值中最小的一个倒推值。
2.1 在进行α-β剪枝时,应注意以下几个问题:
(1)比较都是在极小节点和极大节点间进行的,极大节点和极大节点的比较,或者极小节点和极小节点间的比较是无意义的。
(2)在比较时注意是与"先辈层"节点比较,不只是与父辈节点比较。
当然,这里的"先辈层"节点,指的是那些已经有了值的节点。
(3)当只有一个节点的"固定"以后,其值才能够向其父节点传递。
(4)α-β剪枝方法搜索得到的最佳走步与极小极大方法得到的结果是一致的,α-β剪枝并没有因为提高效率,而降低得到最佳走步的可能性。
(5)在实际搜索时,并不是先生成指定深度的搜索图,再在搜索图上进行剪枝。
如果这样,
就失去了α-β剪枝方法的意义。
在实际程序实现时,首先规定一个搜索深度,然后按照类似于深度优先搜索的方式,生成节点。
在节点的生成过程中,如果在某一个节点处发生了剪枝,则该节点其余未生成的节点就不再生成了。
2.2 α-β剪枝搜索过程(如图3)
在搜索过程中,假定节点的生成次序是从上到下,从左到右进行的。
图中带圈的数字,表示节点的计算次序,在叙述时,为了表达上的方便,该序号也同时表示节点。
当一个节点有两个以上的序号时,不同的序号,表示的是同一个节点在不同次序下计算的结果。
过程如下:
图3 α-β搜索过程的博弈树
图3给出一个d=4的博弈树的α-β搜索过程,其中带圆圈的数字表示求静态估值及倒推值过程的次序编号。
该图详细表示出α-β剪枝过程的若干细节。
初始节点的最终倒推值为1,该值等于某一个端节点的静态估值。
最好优先走步是走向右分枝节点所代表的棋局,要注意棋局的发展并不一定要沿着通向静态值为1的端节点这条路径走下去,这要看对手的实际响应而定。
2.3剪枝的效率问题
若以最理想的情况进行搜索,即对MIN节点先扩展最低估值的节点(若从左向右顺序进行,则设节点估计值从左向右递增排序),MAX先扩展最高估值的节点(设估计值从左向右递减排序),则当搜索树深度为D,分枝因数为B时,若不使用α-β剪枝技术,搜索树的端节点数;若使用α-β剪枝技术.可以证明理想条件下生成的端节点数最少,有
(D为偶数)
(D为奇数)
比较后得出最佳α-β搜索技术所生成深度为D处的端节点数约等于不用α-β搜索技术所生成深度为D/2处的端节点数。
这就是说,在一般条件下使用α-β搜索技术,在同样的资源限制下,可以向前考虑更多的走步数,这样选取当前的最好优先走步,将带来更大的取胜优势。
3.改进方法:
其他改善极小极大过程性能的基本方法:
①不严格限制搜索的深度,当到达深度限制时,如出现博弈格局有可能发生较大变化是,
则应多搜索几层,使格局进入较稳定状态后再终止,这样可使倒推值计算的结果计较
合理,避免考虑不充分产生的影响,这就是等候状态平稳后终止搜索的方法。
②当算法给出所选的走步后,不马上停止搜索,而是在原先估计可能的路径上再往前搜
索几步,再次检验会不会出现意外,这是一种增添辅助搜索的方法。
③某些博弈的开局阶段和残局阶段,往往总结有一些固定的对弈模式,因此可以利用这
些知识编好走不表,以便开局和结局时使用查表法,只是在进入盘中阶段后,再调用
其他有效的搜索算法,来选择最优的走步。
结论:①α-β剪枝过程是二人博弈问题的一般搜索方法,这种方法基于极小极大过程的一些方法都设想对手总是走的最优走步,即我方总是应考虑最坏的情况,从而选出我方最优的应对走法,有效的减少搜索过程中生成的节点数,从而提高了搜索效率。
②本实验所用的博弈搜索技术可用于求解一些双人博弈问题,但是只是一些简单的推理技术,未涉及棋局的表示和启发函数问题。
一些高明棋局中,在一个短期内短兵相接,进攻和防御战术变化剧烈,这些情况怎样在搜索策略中加以考虑。
这就需要我们在往后的学习中不断深化了。
五、基于α-β剪枝的一字棋源代码
#include<iostream>
using namespace std;
int num=0; //记录棋盘上棋子的个数
int p,q;
int tmpQP[3][3]; //表示棋盘数据的临时数组,其中的元素0表示该格为空,
int cur[3][3]; //存储当前棋盘的状态
const int depth=3; //搜索树的最大深度
void Init() //初始化棋盘状态
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
{
cur[i][j]=0;
}
}
void PrintQP() //打印棋盘当前状态
{
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
cout<<cur[i][j]<<'\t';
cout<<endl;
}
}
void UserInput()//用户通过此函数来输入落子的位置,比如:用户输入31,则表示用户在第3行第
1列落子。
{
int pos,x,y;
L1: cout<<"Please input your qizi (xy): ";
cin>>pos;
x=pos/10,y=pos%10;
if(x>0&&x<4&&y>0&&y<4&&cur[x-1][y-1]==0)
{
cur[x-1][y-1]=-1;
}
else
{
cout<<"Input Error!";
goto L1;
}
}
int CheckWin() //检查是否有一方赢棋(返回0:没有任何一方赢;1:计算机赢;-1:人赢){ //该方法没有判断平局
for(int i=0;i<3;i++)
{
if(cur[i][0]==1&&cur[i][1]==1&&cur[i][2]==1)
{
return 1;
}
if(cur[i][0]==-1&&cur[i][1]==-1&&cur[i][2]==-1)
{
return -1;
}
}
for(i=0;i<3;i++)
{
if(cur[0][i]==1&&cur[1][i]==1&&cur[2][i]==1)
{
return 1;
}
if(cur[0][i]==-1&&cur[1][i]==-1&&cur[2][i]==-1)
{
return -1;
}
}
if((cur[0][0]==1&&cur[1][1]==1&&cur[2][2]==1)||(cur[2][0]==1&&cur[1][1]==1&&cur[0][2]==1 ))
{
return 1;
}
if((cur[0][0]==-1&&cur[1][1]==-1&&cur[2][2]==-1)||(cur[2][0]==-1&&cur[1][1]==-1&&cur[0][2 ]==-1))
{
return -1;
}
return 0;
}
int value()//评估当前棋盘状态的值(同时可以用p或q判断是否平局)
{
p=0;
q=0;
for(int i=0;i<3;i++) //计算机一方
{ //将棋盘中的空格填满自己的棋子,既将棋盘数组中的0变为1 for(int j=0;j<3;j++)
{
if(cur[i][j]==0)
{
tmpQP[i][j]=1;
}
else
{
tmpQP[i][j]=cur[i][j];
}
}
}
for(i=0;i<3;i++) //计算共有多少连成3个1的行
{
p+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
}
for(i=0;i<3;i++) //计算共有多少连成3个1的列
{
p+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
}
p+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;//计算共有多少连成3个1的对角线
p+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;
for(i=0;i<3;i++) //人一方
{ //将棋盘中的空格填满自己的棋子,既将棋盘数组中的0变为-1 for(int j=0;j<3;j++)
{
if(cur[i][j]==0)
{
tmpQP[i][j]=-1;
}
else
{
tmpQP[i][j]=cur[i][j];
}
}
}
for(i=0;i<3;i++) //计算共有多少连成3个-1的行
{
q+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
}
for(i=0;i<3;i++) //计算共有多少连成3个1的列
{
q+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
}
q+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;//计算共有多少连成3个1的对角线
q+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;
return p+q; //返回评估出的棋盘状态的值
}
int cut(int &val,int dep,bool max)//主算法部分,实现a-B剪枝的算法,val为上一层的估计值,dep 为搜索深度,max记录上一层是否为极大层
{
if(dep==depth||dep+num==9) //如果搜索深度达到最大深度,或者深度加上当前棋子数已经达到9,就直接调用估计函数
{
return value();
}
int i,j,flag,temp; //flag记录本层的极值,temp记录下层求得的估计值
bool out=false; //out记录是否剪枝,初始为false
/*if(CheckWin()==1) //如果计算机赢了,就置上一层的估计值为无穷(用很大的值代表无穷)
{
val=10000;
return 0;
}*/
if(max) //如果上一层是极大层,本层则需要是极小层,记录flag为无穷大;反之,则为记录为负无穷大
{
flag=10000; //flag记录本层节点的极值
}
else
{
flag=-10000;
}
for(i=0;i<3 && !out;i++) //双重循环,遍历棋盘所有位置
{
for(j=0;j<3 && !out;j++)
{
if(cur[i][j]==0) //如果该位置上没有棋子
{
if(max) //并且上一层为极大层,即本层为极小层,轮到用户玩家走了。
{
cur[i][j]=-1; //该位置填上用户玩家棋子
if(CheckWin()==-1) //如果用户玩家赢了
{
temp=-10000; //置棋盘估计值为负无穷
}
else
{
temp=cut(flag,dep+1,!max); //否则继续调用a-B剪枝函数
}
if(temp<flag) //如果下一步棋盘的估计值小于本层节点的极值,则置本层极值为更小者
{
flag=temp;
}
if(flag<=val) //如果本层极值已小于上一层的估计值,则不需搜索下去,剪枝
{
out=true;
}
}
else //如果上一层为极小层,即本层为极大层,轮到计算机走了。
{
cur[i][j]=1; //该位置填上计算机棋子
if(CheckWin()==1) //如果计算机赢了
{
temp=10000; //置棋盘估计值为无穷
}
else
{
temp=cut(flag,dep+1,!max);//否则继续调用a-B剪枝函数
}
if(temp>flag)
{
flag=temp;
}
if(flag>=val)
{
out=true;
}
}
cur[i][j]=0; //把模拟下的一步棋还原,回溯
}
}
}
if(max) //根据上一层是否为极大层,用本层的极值修改上一层的估计值{
if(flag>val)
{
val=flag;
}
}
else
{
if(flag<val)
{
val=flag;
}
}
return flag; //函数返回的是本层的极值
}
int main()//主程序
{
int m=-10000,val=-10000,dep=1; //m用来存放最大的val
int x_pos,y_pos; //记录最佳走步的坐标
Init();
cout<<"Qipan: "<<endl;
PrintQP();
char IsFirst;
cout<<"Do you want do first?(y/n)";
cin>>IsFirst;
while(IsFirst!='y'&&IsFirst!='n')
{
cout<<"ERROR!"<<"Do you want do first?(y/n)";
cin>>IsFirst;
}
if(IsFirst=='n')//---------------------------------计算机先走--------------------------------------
{
L5: for(int x=0;x<3;x++)
{
for(int y=0;y<3;y++)
{
if(cur[x][y]==0)
{
cur[x][y]=1;
cut(val,dep,1);//计算机试探的走一步棋,棋盘状态改变了,在该状态下计算出深度为dep-1的棋盘状态估计值val
if(CheckWin()==1)
{
cout<<"The computer put the qizi at:"<<x+1<<y+1<<endl;
PrintQP();
cout<<"The computer WIN! GAME OVER."<<endl;
return 0;
}
if(val>m) //m要记录通过试探求得的棋盘状态的最大估计值
{
m=val;
x_pos=x;y_pos=y;
}
val=-10000;
cur[x][y]=0;
}
}
}
cur[x_pos][y_pos]=1;
val=-10000;
m=-10000;
dep=1;
cout<<"The computer put the qizi at:"<<x_pos+1<<y_pos+1<<endl;
PrintQP();
cout<<endl;
num++;
value();
if(p==0)
{
cout<<"DOWN GAME!"<<endl;
return 0;
}
UserInput(); //玩家走一步棋
PrintQP();
cout<<endl;
num++;
value();
if(p==0)
{
cout<<"DOWN GAME!"<<endl;
return 0;
}
if(CheckWin()==-1)
{
cout<<"Conguatulations! You Win! GAME OVER."<<endl;
return 0;
}
goto L5;
}
else //--------------------------------人先走----------------------------------- {
L4: UserInput();
PrintQP();
cout<<endl;
num++;
value();
if(q==0)
{
cout<<"DOWN GAME!"<<endl;
return 0;
}
if (CheckWin()==-1)
{
cout<<"You Win! GAME OVER."<<endl;
return 0;
}
for(int x=0;x<3;x++)
{
for(int y=0;y<3;y++)
{
if(cur[x][y]==0)
{
cur[x][y]=1;
cut(val,dep,1);
if(CheckWin()==1)
{
cout<<"The computer put the qizi at:"<<x+1<<y+1<<endl;
PrintQP();
cout<<"The computer WIN! GAME OVER."<<endl;
return 0;
}
if(val>m)
{
m=val;
x_pos=x;y_pos=y;
}
val=-10000;
cur[x][y]=0;
}
}
}
cur[x_pos][y_pos]=1;
val=-10000;
m=-10000;
dep=1;
cout<<"The computer put the qizi at:"<<x_pos+1<<y_pos+1<<endl;
PrintQP();
cout<<endl;
num++;
value();
if(q==0)
{
cout<<"DOWN GAME!"<<endl;
return 0;
}
goto L4;
}
return 0;
}。