最清晰易懂的MinMax算法和Alpha-Beta剪枝详解

最清晰易懂的MinMax算法和Alpha-Beta剪枝详解

参考文章

http://web.cs.ucla.edu/~rosen/161/notes/alphabeta.html
https://www.cnblogs.com/pangxiaodong/archive/2011/05/26/2058864.html

一、MinMax算法

  Minimax算法(亦称 MinMax or MM)又名极小化极大算法,是一种找出失败的最大可能性中的最小值的算法。

  Minimax算法常用于棋类等由两方较量的游戏和程序。该算法是一个零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,另一方则选择令对手优势最小化的方法。而开始的时候总和为0。很多棋类游戏可以采取此算法,例如井字棋(tic-tac-toe)。


  以上是来自WIKI百科的解释,其实说的通俗一点,就是有两个人下棋,先手希望下一步的局面是自己胜算最大的局面,而后手则希望下一步的局面是先手胜算最小的局面(因为先手输,后手就会赢)。

  那么,如何来判断当前的局面的胜算到底怎么样呢?我们可以设置一个估价函数,按照某种方案来计算当前局面的优劣程度。

  (建议先跳过以下内容)

  然后在这种想法的前提下,就是高能的博弈过程。如果只向后看一步(也就是直接计算下一步的估价值)的话,那情况就是:先手预测了自己走完这一步的所有可能的局面,然后选择了所有走法中局面看起来最好的(估价函数的结果最好的)走法。之后,后手也预测了自己走完这一步的所有可能的局面,然后也选择了所有走法中局面看起来最好的(估价函数的结果最好的)走法。如此循环往复,直到最后游戏结束。(GG,即Good Game)

  如果这两个棋手比较优秀,可以往后看两步(也就是只直接计算往后数两步的估价值),那么,情况就是这样的:先手预测了自己走完这一步的所有可能局面,并同时预测了对手的所有应对方案(也就是对手会选择让下一步局面估价函数最小的走法,所以先手这一步应该选择,让后手不管走么走估价函数都尽量大的走法),综合这些信息,选择了一个最优的局面,后手也是如此。也就是我猜到了我这么走,你会怎么走,所以我选择这么走的高端玩法。

  当然,也可以往后看三步(也就是只直接计算往后数三步的估价值),那么,情况就是这样的:先手预测了自己走完这一步的所有可能局面,并同时预测了对手的所有应对方案,还同时又想到了自己在面对对手的每种应对方案时的所有可能走法,从中选择一个最优秀的,后手也是如此。也就是,我猜到了我这么走,你会那么走,然后我会这么走,所以我选择这么走的更高端玩法。


  (跳到这儿就可以了)

  不知道大家是不是懵逼了,其实看不懂也没关系,上面的大家可以当成废话那你写这么多干嘛?MDZZ),下面我们就要上图啦,结合图示,一下子就可以明白怎么回事了。

  我们用正方形来代表先手(选择估价最大的局面),圆形来代表后手(选择估价最小的局面)只有叶子节点才可以直接计算估价值

  则我们假设棋局的博弈树如下(往后看4步),那么先手应该如何选择呢:

在这里插入图片描述

  首先,先手应该计算后手在第四步的时候应该会选择价值为多大的局面(即从所有子节点中选择最小的),如下图(红色字体):

在这里插入图片描述

  然后先手在计算自己在第三步时应该如何选择(即从所有子节点中选择最大的),如下图(红色字体):

在这里插入图片描述

  然后先手在计算后手在第三步时应该如何选择(即从所有子节点中选择最小的),如下图(红色字体):

在这里插入图片描述

  最后先手就可以决定下一步应该怎么走了(即从所有子节点中选择最大的),如下图(红色字体):

在这里插入图片描述

  所以,如果先后手都进行最有决策的话,棋局的走向则下图所示(红线):

在这里插入图片描述

  (当然,如果后手不是进行最优决策的话,棋局的走向就不一定是这样的了,先手也可以尝试走有风险,但是收益更高的局面,这就不在我们的讨论范围之内了)

  可以看到,如果按照MinMax算法来进行决策的话,需要的计算量是随着向后看的步数的增加而呈指数级增长的。但是,这些状态中其实是包含很多不必要的状态的,所以我们可以进行剪枝。

注:真实的搜索顺序并不一定是一层一层的进行的

二、Alpha-Beta剪枝

   Alpha-beta( α − β \alpha - \beta αβ)剪枝的名称来自计算过程中传递的两个边界,这些边界基于已经看到的搜索树部分来限制可能的解决方案集。 其中,Alpha( α \alpha α)表示目前所有可能解中的最大下界,Beta( β \beta β)表示目前所有可能解中的最小上界。

  因此,如果搜索树上的一个节点被考虑作为最优解的路上的节点(或者说是这个节点被认为是有必要进行搜索的节点),那么它一定满足以下条件( N N N是当前节点的估价值):
α ≤ N ≤ β \alpha \leq N \leq \beta αNβ
  在我们进行求解的过程中, α \alpha α β \beta β会逐渐逼近。如果对于某一个节点,出现了 α > β \alpha > \beta α>β的情况,那么,说明这个点一定不会产生最优解了,所以,我们就不再对其进行扩展(也就是不再生成子节点),这样就完成了对博弈树的剪枝。

  可能这个过程描述起来还是有一些抽象,那么,我们就用图像(依旧是往后看4步)来还原模拟一下这个过程:

  首先,我们开始从根节点向下进行搜索,直到进行了四步为止(废话,不到4步没有值呀),红色的序号代表顺序。

在这里插入图片描述

  当我们搜索到了第一个叶子节点的时候,我们发现它的权值是3,并且它的父节点是Min节点,又因为父节点的最小上界 β > 3 \beta > 3 β>3,所以我们更新父节点,令 β = 3 \beta = 3 β=3。(因为父节点要取最小值,这个最小值不会比3更大,所以我们更新其上界)然后继续向下搜索。

在这里插入图片描述

  因为17比3大,所以这个节点我们可以无视掉(没啥用啊)。我们已经搜索完了当前这个Min节点的所有孩子,所以我们返回它的节点值给它的父节点(Max节点),尝试更新父节点的 α \alpha α值。(因为这是一个Max节点,他的孩子的估价值和 α 、 β \alpha、\beta αβ值已经确定了,所以父节点取值范围的下界也需要被更新),此处更新父节点,令 α = 3 \alpha = 3 α=3。然后继续进行搜索,注意新生成的子节点的 α 、 β \alpha、\beta αβ 值继承自父节点

在这里插入图片描述

  继续搜索,至叶子节点:

在这里插入图片描述

  我们发现它的权值是2,并且它的父节点是Min节点,又因为父节点的最小上界 β > 2 \beta > 2 β>2,所以我们更新父节点,令 β = 2 \beta = 2 β=2。(因为父节点要取最小值,这个最小值不会比2更大,所以我们更新其上界)。然后此时我们发现父节点出现了 α > β \alpha > \beta α>β的情况,说明最优解不可能从这个节点的子节点中产生,所以我们不再继续搜索它的其他子节点。(这就是 β \beta β剪枝)并继续返回其节点值,尝试更新父节点。因为父节点的 α = 3 > 2 \alpha = 3 > 2 α=3>2,所以更新失败。

  然后我们已经搜索完了当前这个Max节点的所有子节点,所以我们返回它的节点值,并尝试更新他的父节点的 β \beta β 值。因为父节点的 β > 3 \beta > 3 β>3,所以我们令$\beta = 3 $。并继续向下搜索至叶子节点,注意新生成的子节点的 α 、 β \alpha、\beta αβ 值继承自父节点

在这里插入图片描述

  然后,我们发现15并不能更新当前节点的 β \beta β值,所以令当前节点权值为15,并返回其权值,尝试更新其父节点(Max节点)的 α \alpha α值。因为其父节点的 α < 15 \alpha < 15 α<15,所以我们令$\alpha = 15 $。

  此时,该节点 α = 15 , β = 3 \alpha = 15 , \beta = 3 α=15,β=3 α > β \alpha > \beta α>β,则说明其子节点并不包含最优解,不需要在进行搜索。所以返回其节点权值给父节点(Min节点),尝试对父节点的 β \beta β值进行更新。父节点的 β < 15 \beta < 15 β<15,则不需要进行更新。同时可确定父节点的权值为3。

在这里插入图片描述

  继续返回权值给父节点,尝试更新父节点的 α \alpha α,发现父节点 α < 3 \alpha < 3 α<3,则令 α = 3 \alpha = 3 α=3,并继续向下搜索直至叶子节点。注意新生成的子节点的 α 、 β \alpha、\beta αβ 值继承自父节点

在这里插入图片描述

  从叶子节点返回权值给父节点(Min节点),并尝试更新其父节点的 β \beta β值,因为父节点 β > 2 \beta > 2 β>2,所以,令 β = 2 \beta = 2 β=2,此时有 α > β \alpha > \beta α>β,说明其子节点并不包含最优解,不需要再进行搜索。所以返回其节点权值给父节点(Max节点),尝试对父节点的 α \alpha α值进行更新。因为父节点 α > 2 \alpha > 2 α>2,无需进行更新,继续搜索其子节点至叶子节点。

在这里插入图片描述

  从叶子节点返回权值给父节点(Min节点),并尝试更新其父节点的 β \beta β值,因为父节点 β > 3 \beta > 3 β>3,所以,令 β = 3 \beta = 3 β=3,同时确认父节点权值为3。

  继续返回权值给父节点,并尝试更新其父节点的 α \alpha α值,因为父节点 α = 3 \alpha = 3 α=3,所以无需进行更新,同时确定该节点权值为3。

  因为该节点的所有子节点全部搜索完毕,所以返回该点权值给父节点,并尝试更新其父节点的 β \beta β值,因为父节点 β > 3 \beta > 3 β>3,所以,令 β = 3 \beta = 3 β=3,同时确认父节点权值为3。因为此时有 α = β = 3 \alpha = \beta = 3 α=β=3,所以无需再搜索其子节点,直接返回权值给根节点,并尝试更新根节点的 α \alpha α值,因为根节点 α = 3 \alpha = 3 α=3,所以无需进行更新。

  根节点的所有子节点搜索完毕,则得出最优解为3。

在这里插入图片描述

  可以对比一下不加剪枝与加了剪枝之后的搜索过程,可以发现Alpha-Beta剪枝对算法的性能有了很大的提升。

三、代码

伪代码如下:

MinMax算法:

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 α := +∞
       foreach 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 cut-off *)
        return α
    else // 极小节点
        for each child of node // 极大节点
            β := min(β, alphabeta(child, depth-1, α, β, not(Player) )) // 极小节点
            if β ≤ α // 该极大节点的值<=β<=α,该极小节点后面的搜索到的值肯定会小于α,因此不会被其上层的极大节点所选用了。对于根节点,α为负无穷
                break                             (* Alpha cut-off *)
        return β 
(* Initial call *)
alphabeta(origin, depth, -infinity, +infinity, MaxPlayer)
  • 248
    点赞
  • 728
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
======================================================================== 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. /////////////////////////////////////////////////////////////////////////////

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值