【学习笔记】Min-Max搜索与α-β剪枝

我们在解决ICG游戏问题时,引入了 S G SG SG函数作为必胜策略的判断依据。但 S G SG SG函数的局限在于时空复杂度。 S G SG SG搜索固然能保证答案的正确性,但其时空复杂度与状态数正相关。当游戏的状态数很多时, S G SG SG搜索便不再适用。此时应当适当牺牲正确性来保证高效性和可行性。

Min-Max搜索

为在牺牲正确性的同时保证可行性,我们可以引入估值函数 f ( x ) : S → R f(x):S\to R f(x):SR给出对某一个状态 S S S的评估。

我们规定,若状态 S ′ S' S可以由状态 S S S转移而来,则 f ( S ′ ) f(S') f(S)给出了相对于S先手的估值。这个值越大认为 S S S局面(注意不是 S ′ S' S)越优。特别地,当 S ′ S' S先手必败时,则对 S S S而言先手必胜,则此时估值可以设置为 + ∞ +\infty +

我们将不同状态之间的转移关系抽象为有向边,便可以从初始状态 S 0 S_0 S0开始建立一棵搜索树,每一个节点代表一个状态。

在ICG游戏中,仅会有一个玩家胜利,即满足零和性。由此我们可以得到下列转移性质:

  1. 对于根节点 S 0 S_0 S0,认为其深度为0,是先手对应的状态。
  2. 对于叶子节点 l l l,其估值为 f ( l ) f(l) f(l) f ( l ) f(l) f(l)与对先手的有利程度有关。
  3. 对于深度为 d d d的内部节点 n n n
    1. d d d是偶数,则 f ( n ) = max ⁡ s ∈ c h [ n ] f ( s ) f(n)=\max_{s\in ch[n]}f(s) f(n)=maxsch[n]f(s)
    2. d d d是奇数,则 f ( n ) = min ⁡ s ∈ c h [ n ] f ( s ) f(n)=\min_{s\in ch[n]}f(s) f(n)=minsch[n]f(s)

前两点较易理解。对于第三点理解如下:经过每一轮,先后手会发生改变,因此在搜索树上,根节点等偶数层的点代表先手行动,而奇数层的点代表后手行动。由游戏的零和性,双方都需要找到对自己最有利的策略,即是对对方最不利的策略。而整个搜索树的估值函数是针对于先手而言的,当估值函数越小,则先手越不利。因此在后手行动的奇数层,就需要找到后继中估值函数值最小的状态进行转移,从而尽可能使后手最优。对偶数层同理。故得到上述转移函数。

一个搜索树示例如下:(图源:《极大极小搜索算法 minimax search》by thormas1996,https://blog.csdn.net/thormas1996/article/details/102662222)

【学习笔记】Min-Max搜索与α-β剪枝_第1张图片

可以发现,上述策略选择的正确性与估值函数的选取有着重要联系。在上图中,为使先手获得的利益最大,则应当选取 A − C − H A-C-H ACH这一条路径。

α − β \alpha-\beta αβ剪枝

容易发现, M i n − M a x Min-Max MinMax搜索的状态数仍然可能很多,因此需要采用一些剪枝操作,其中较有名的便是 α − β \alpha-\beta αβ剪枝。

α − β \alpha-\beta αβ剪枝的基本思想是将搜索树上的点进行分类,通过对某一个点的可能估值确定上下界从而排除不可能的分支,达到剪枝的目的。

我们将搜索树上的节点分为三类:

  1. 已遍历完自身及其子树,得到了自身的估值的点。不妨称为A类点。
  2. 未完全遍历完自身子树,但已有至少一个儿子已完全遍历并确定了估值,即儿子中至少有一个A类点。我们称这类点为B类点。
  3. 未遍历该点或该点的儿子中未有已确定估值的,即无A类点儿子。我们称这类点为C类点。(C类点可能有B类点儿子)

$\alpha-\beta $剪枝的针对对象便是B类点。它们的估值并未确定,但由于部分儿子的估值已经确定,所以可以根据这些信息为B类点的估值范围做出限制,并依此排除不可能的分支。

我们定义每一个B类点的估值的取值范围都有下界 α \alpha α和上界 β \beta β。当某一个分支的解必定小于 α \alpha α或大于 β \beta β时,则将这个分支剪除。

根据上述定义,我们需要解决两个问题:

  1. 每一个B类点的上下界应当如何确定?
  2. 如何根据上下界进行剪枝?

确定每一个点的上下界

我们先解决第一个问题。先给出结论:B类点的上下界由儿子与父亲共同决定。其中儿子节点会给定具体值来影响B类点的上下界,而父亲节点则是直接让B类点继承其上下界

先举一个例子方便理解:
【学习笔记】Min-Max搜索与α-β剪枝_第2张图片
初始化所有节点的下界为负无穷,上界为正无穷,在搜索时现在搜索到上图阶段,即是由B号点访问值为3的叶子节点完毕后进行回溯的过程。

此时B号点属于max层,因此它最终的取值必定会大于等于其中某一个儿子的取值,由其第一个儿子可以确定B的取值范围下界为3。

上图反映了B类点上下界确定的第一种方法:由儿子节点确定的返回值来影响B类点的上下界
【学习笔记】Min-Max搜索与α-β剪枝_第3张图片

上图是B号点在搜索完毕之后,进一步回溯到父亲A号点。

由于A是在min层,因此其最终的取值必定小于等于其某一个儿子的取值,故由B号点的取值可以确定A的取值上界为5。

【学习笔记】Min-Max搜索与α-β剪枝_第4张图片

上图反映的是A号点在访问完B号点之后,即将访问C号点的情况。由于A号点已经初步确定了自己的上下界(上界为5),所以它将会把这个上下界直接赋予到儿子节点C号点上。赋予这个上下界的意义是:C的可能取值如果大于5,则没有意义,因为在C所在的层有值为5的B号点,B号点一定比C号点更优。因此我们可以令C的取值上界为5,从而对C引出的分支中取值大于5的分支进行剪枝。

小结一下:每一个点的上下界的确定有两种影响因素:

  1. 由儿子节点中已知的点来确定上下界取值范围。
  2. 由父亲节点直接继承上下界。

对于第一种,如果儿子存在多种取值的话,新确定的取值可能会进一步减小父亲的取值范围。如下图,一开始确定了B号点的取值为5之后,A的取值范围为 ( − ∞ , 5 ] (-\infty,5] (,5],在确定了C号点的取值为3之后,A的取值范围便缩小为 ( − ∞ , 3 ] (-\infty,3] (,3]

【学习笔记】Min-Max搜索与α-β剪枝_第5张图片

当然,也可能存在新确定的儿子取值与已有的取值区间冲突的情况,这就引出了我们的第二个问题。

如何根据上下界进行剪枝

我们以一个例子来展示这种剪枝方法的进行流程以及其优越性。
【学习笔记】Min-Max搜索与α-β剪枝_第6张图片

上图中,我们确定了C号点的取值范围,接下来遍历C号点的子树的时候,第一个儿子取值为10,超出了C的上界,那么C的取值一定会大于等于10,故C的分支一定是一个无用分支(A一定会选择B号点作为更优的选择),因此我们可以直接停止遍历C的子树,并令其返回一个必定不会成为答案的值(C处于最大层,我们默认返回其上界),从而完成了对C的子树搜索的剪枝。

注意:C位于max层,故若C的子元素的取值小于C的下界,不能直接进行剪枝。

【学习笔记】Min-Max搜索与α-β剪枝_第7张图片

上图中C遍历完自己的第一个儿子之后,取值区间为 [ 3 , 5 ] [3,5] [3,5]。第二个儿子的取值小于下界3,此时不能进行剪枝,因为不能保证C的分支一定是无效分支,可以看到后续的第三个儿子取值为4,恰好是C号点的最优策略,也是A的最优策略。

上述剪枝的效果是十分明显的,尽管在图上未有明显的体现。但如果C的另一个儿子对应的搜索子树十分庞大,用这种剪枝方法便可以避免对这个子树的遍历,从而大大减小了搜索可能状态,提高效率。

注意:如果某一个点成功遍历完了所有的儿子且并未被剪枝,那么返回的时候并不是返回这个点自身的上界或者下界,而是返回其所有儿子节点中的最大值或者最小值。即该点的取值上界不一定是其儿子节点的最大值。(从上图便可以看出,C号点的最大儿子为4,但是上界为5,此时返回C号点的取值应该是4)

小结

从上述的两个问题的解决过程,我们可以总结到 α − β \alpha-\beta αβ剪枝的基本规则:

  1. 若x处于max层,其儿子节点的估值大于x的下界值,则更新下界。
  2. 若x处于max层,其儿子节点的估值大于x的上界值,则停止遍历x的子树,返回x的上界值。
  3. 若x处于min层,其儿子节点的估值小于x的上界值,则更新上界。
  4. 若x处于min层,其儿子节点的估值小于x的下界值,则停止遍历x的子树,返回x的下界值。
  5. 若x的所有儿子遍历完毕,则根据x所在层返回其儿子节点取值的最大值或者最小值。

实现代码如下:

void alpha_beta(int node, bool player, int alpha, int beta)
{
    if(!val[node])//如果当前点未确定估值	
    {
        int maxx = -inf, minn = inf;//记录node节点的儿子节点取值的最大值或者最小值
        for( int i = 0; i < children[node].length; i++ )
        {
            int now_val = alpha-beta(childern[node][i], player ^ 1,	alpha, beta);//继承上下界
            maxx = max(maxx, now_val);
            minn = min(minn, now_val);
            if(player == 0)//对应的是最大层
            {
               	if(now_val > alpha)
                {
                    alpha = now_val;
                }
                if(alpha >= beta)
                {
                    return beta;
                }
            }
            else //对应的是最小层
            {
                if(now_val < beta)
                {
                    beta = now_val;
                }
                else 
                {
                    return alpha;
                }
            }
        }
       	val[node] = (player == 1 ? minn : maxx);
    }
    return val[node];
}

你可能感兴趣的:(学习,剪枝,算法)