Alpha-Beta剪枝算法在直棋中的运用

游戏说明

详见本人的项目描述页面:https://gitee.com/liuweilhy/NineChess/blob/master/Readme.md
我做了个简单的GUI如下图。
GUI
点击这里可以下载运行程序,有Windows x64和Linux x64两个并行版本。注意自行挑选最新的发行版。

棋类游戏的特点

像黑白棋、五子棋、象棋、围棋等棋类游戏,是典型的零和博弈:

  1. 信息完备,过程和结果都是确定性的,没有随机运气成分(不同于大部分牌类游戏);
  2. 双方对弈,轮流行动;
  3. 此消彼长,对一方有利的即对另一方有害,量化后的双方分数之和始终为0。

这类零和博弈,如果采用传统的递归回溯算法,其计算量是十分巨大的。如果对不必要计算的分支进行剪除,可以减少计算量。本文采用Alpah Beta剪枝算法来求解。

Alpha-Beta剪枝算法

关于Alpha-Beta剪枝算法,网上的教程很多,不再赘述。可以参照:
http://www.cnblogs.com/speeding/archive/2012/09/20/2694704.html
其实这个算法跟人类的下棋思路是接近一致的:

  1. 轮到我走,我肯定要走对我最有利的一步(但我不确定这一步对我是否有利);
  2. 如果我走这一步的话,对手肯定要走对我最不利的一步;
  3. 如果对手走了对我最不利的一步,我再走一步扳回优势的一步;
  4. …………
  5. 在演算过程中,那些我跟对手都不会走的“昏招”要排除掉;
  6. 假设我能算到第5步,我就知道第4步怎么走最合适,进而知道第3步、第2步,最终决定接下来第1步怎么走。

算法要点(认真看,就懂了)

  1. 节点node:代表选择出的最下层局面的评价(叶子节点值);
  2. 连线move:代表可选择的招法;
  3. 深度depth:将底层设为0,向上递增(当然也可以用顶层根节点为0,向下递增),越深则越容易得出优质的解,但更耗费时间;
  4. 节点和节点下的连线(招法)处于同一层中;
  5. 招法是自上而下的,但节点评估是自下而上的,所以只能深度优先;
  6. 对叶子节点的评价值,有利于先手的为正数,有利于后手的为负数;
  7. 每个节点都有一个alpha值和beta值,分别表示节点所在层可以最终选出的最大和最小评价值;
  8. alpha初始设为-∞,beta初始设为+∞;
  9. 对于Max层,不用计算beta值,因为先手不会选最小评价值;
  10. 同样,对于Min层,不用计算alpha值,因为后手不会选最大评价值;
  11. 对于Max层的节点,取值是所有子节点中的最大值(即先手要选择最有利于自己的局面);
  12. 同样,对于Min层的节点,取值是所有子节点中的最小值(即后手要选择最不利于先手的局面);
  13. Max层的节点在遍历子节点找alpha的时候,如果发现子节点中有大于beta值的节点,则放弃遍历。因为它的父节点在Min层,Min层节点不会选择一个大于已有节点的节点(即对手不会选择更有利于先手的局面),所有没有必要继续找下去,在此剪枝。这一步是剪枝算法的核心!
  14. 同样,Min层的节点在遍历子节点查找beta的时候,如果发现子节点中有小于alpha值的节点,则放弃遍历,在此剪枝。这一步同样是剪枝算法的核心!
  15. 简单来讲,alpha和beta值在初始化的时候,是父节点直接传递给子节点的;而子节点回传时,Min层节点将自己的beta值传给父节点,而Max层节点将自己的alpha值传给父节点;Min层父节点挑选回传值中最小的一个,跟当前层beta值比较,用较小值重新设置beta;Max层父节点挑选回传值中最大的一个,跟当前层alpha值比较,用较大值重新设置alpha;如果任一节点发现当前alpha>beta,则放弃遍历。

GitHub上有个算法过程演示,很容易理解:
https://github.com/aykamko/abTreePractice
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

MiniMax搜索和NegaMax搜索

上面提到的算法,双方评估函数相同(利于先手为正、利于后手为负),但区分Max层和Min层,在Max层取alpha值,在Min层取Beta值,我们称之为MiniMax搜索。这种搜索方法比较容易理解。

value = alphaBetaPruning(p, alpha, beta, !who); //取子节点的值

另外有还有一种NegaMax搜索方式,双方评价函数不同(利于自己为正、利于对手为负),不区分Maxx层和Min层,全部取alpha值。这种搜索方法写起来更简洁,网上很多Alpha-Beta剪枝算法采用它,但它需要修改评价函数,使评价函数根据当前选手来定(先手不变,后手取反)。

value = -alphaBetaPruning(p, -beta, -alpha, !who); //取子节点的值

直棋中需要额外注意的要点

对于直棋(九连棋、成三棋、莫里斯棋)而言,有不同于常规Alpha-Beta算法的地方:

Max层和Min层不一定是严格交替分布的:
一方行棋成三后可能要继续执行去子,甚至还有九连棋这样闷死让行的,这会形成连续两层以上的Max或Min,也会有在同一层既有Max也有Min的情况
不过这不影响搜索算法,我们只需要在取值前确定这是谁的回合就好。

排序函数与出招的随机化

Alpha-Beta剪枝算法是从前向后深度优先搜索,越早发现最优节点,后面就会越早发生剪枝,进而节省大量时间。
如何将可能出现最优节点的招法放在前面,这里就需要对子节点进行排序了。(我的算法中,没有具体实现优化排序)
另外,排序的随机化,会使出招随机化,表现为AI不会每一局都出相同的招。

节点层次评估与迭代加深

在代码测试阶段,我发现当一方胜券在握的情况下,偶尔会进入循环僵局,类似于象棋中的“长将”。
分析原因:假设胜券在握的一方检索6层深度,它在第3层和第5层都发现了决胜招,不考虑随机出招的情况,算法会走它发现第一个决胜招法,而第一个决胜招法可能会不断加深,形成循环。
我们有两种方案解决这个问题:

节点层次评估

常规算法中,第3层和第5层的决胜局面评分是相同的。我们对评估值进行一个优化,减去深度值,使第3层的评分高于第5层的评分。这样,算法就会选择更短的决胜出招路径了。

迭代加深

不直接将检索深度设置为预置深度,而是从1层算起,然后2、3、4、5层……依次加深,如果找到决胜招数,就不再计算更深的层次了。
这个招法会有额外的计算开销(算第2层时,1层的局面要重新生成),但在终局阶段,由于其不再进行更深度的无谓检索,实际开销往往反而低于固定深度搜索。

等价局面与置换表

  1. 对当前局面的评估,仅仅与局面本身有关,与形成此局面的招法顺序无关,如下面的两种招法形成的局面是一致的:
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    path2
  2. 对任一局面,它的镜像局面跟它的评价值是相同的,如:
    mirror
  3. 对任一局面,它的内外层翻转局面跟它的评价值是相同的,如:
    turn
  4. 对任一局面,它的3个旋转局面跟它的评价值是相同的,如:
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    对这类等价局面,重复搜索会浪费大量的时间。
    我们建立一个特定大小的保存了局面信息的Hash表,每次查找新局面时,都在表中查找有无等价局面,有的话直接取其值即可。这样就节省了重复计算的时间。
    Hash表使用C++11里的unordered_map。Hash的计算有待完善。

算法的缺点

  1. 计算量的大小严重依赖于着法的寻找顺序,但并没有足够好的排序算法能在很小的开销下实现最优排序。
  2. 非最终局面时的评价函数,往往是编程者的主观评价,并不严谨。评价函数直接决定了剪枝,有可能把最优的那一枝剪掉了。
  3. 因层深限制不能遍历最终局面时,Alpha-Beta剪枝算法并不能得到最优解。

对于围棋,如果采用Alpha-Beta剪枝算法,计算量是巨大的,尤其是开局和中局这种空位置很多的阶段(不考虑开局库),所以能算到的深度很小(恐怕连10层都没有)。
人类在这种大广度的游戏中,往往可以靠经验而非计算,来剪掉大量的“枝”。在这种情况下,经验(开局库)往往比算法更具优势。
在《天龙八部》中,无崖子所创珍珑棋局,天下高手无人可破,却被虚竹随便破解。为什么?因为虚竹下的是明显的“昏招”,评价函数返回了一个很大的负数,所以高手们都不会选的。只有计算的深度再增加几层,算法才会发现这步“昏招”其实是评分为正数的“妙招”。
这就是在神经网络算法的AlphaGo面世前,所有采用Alpha-Beta剪枝算法的AI都难以在围棋上战胜人类大师的根本原因。

我的算法实现

作为一个非IT界的外行人,我的Alpha-Beta剪枝算法写得一般般,勉强能用吧。
源码相见我的NineChess项目

  • NineChess:棋的模型类,用标准C++编写,适用于各个编译平台;
  • NineChessAi_ab:Alpha-Beta剪枝算法类,也用标准C++编写,适用于各个编译平台;

我的剪枝算法暂未实现招法优化排序,置换表因效率问题取消掉了,所以算力有限,耗时较长。以后有有时间再继续改善吧。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
======================================================================== MICROSOFT FOUNDATION CLASS LIBRARY : fir ======================================================================== AppWizard has created this fir application for you. This application not only demonstrates the basics of using the Microsoft Foundation classes but is also a starting point for writing your application. This file contains a summary of what you will find in each of the files that make up your fir application. fir.dsp This file (the project file) contains information at the project level and is used to build a single project or subproject. Other users can share the project (.dsp) file, but they should export the makefiles locally. fir.h This is the main header file for the application. It includes other project specific headers (including Resource.h) and declares the CFirApp application class. fir.cpp This is the main application source file that contains the application class CFirApp. fir.rc This is a listing of all of the Microsoft Windows resources that the program uses. It includes the icons, bitmaps, and cursors that are stored in the RES subdirectory. This file can be directly edited in Microsoft Visual C++. fir.clw This file contains information used by ClassWizard to edit existing classes or add new classes. ClassWizard also uses this file to store information needed to create and edit message maps and dialog data maps and to create prototype member functions. res\fir.ico This is an icon file, which is used as the application's icon. This icon is included by the main resource file fir.rc. res\fir.rc2 This file contains resources that are not edited by Microsoft Visual C++. You should place all resources not editable by the resource editor in this file. ///////////////////////////////////////////////////////////////////////////// For the main frame window: MainFrm.h, MainFrm.cpp These files contain the frame class CMainFrame, which is derived from CFrameWnd and controls all SDI frame features. ///////////////////////////////////////////////////////////////////////////// AppWizard creates one document type and one view: firDoc.h, firDoc.cpp - the document These files contain your CFirDoc class. Edit these files to add your special document data and to implement file saving and loading (via CFirDoc::Serialize). firView.h, firView.cpp - the view of the document These files contain your CFirView class. CFirView objects are used to view CFirDoc objects. ///////////////////////////////////////////////////////////////////////////// Other standard files: StdAfx.h, StdAfx.cpp These files are used to build a precompiled header (PCH) file named fir.pch and a precompiled types file named StdAfx.obj. Resource.h This is the standard header file, which defines new resource IDs. Microsoft Visual C++ reads and updates this file. ///////////////////////////////////////////////////////////////////////////// Other notes: AppWizard uses "TODO:" to indicate parts of the source code you should add to or customize. If your application uses MFC in a shared DLL, and your application is in a language other than the operating system's current language, you will need to copy the corresponding localized resources MFC42XXX.DLL from the Microsoft Visual C++ CD-ROM onto the system or system32 directory, and rename it to be MFCLOC.DLL. ("XXX" stands for the language abbreviation. For example, MFC42DEU.DLL contains resources translated to German.) If you don't do this, some of the UI elements of your application will remain in the language of the operating system. /////////////////////////////////////////////////////////////////////////////

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值