🏠 总目录📚 本教程 11 · α-β 剪枝
📑 本页目录(点开跳转)

11 · 剪枝

34 分钟 | ⭐⭐ 结果和不剪枝一模一样,但同样的时间能多看一倍深度


🎯 一句话

搜索时随身带一个区间 $[\alpha, \beta]$,值落到区间外的分支整条丢掉 —— 根的值和选出的走法和 Minimax 完全一致,而在理想的走法顺序下复杂度从 $O(b^m)$ 降到 $O(b^{m/2})$。

这就是《博弈论与集体决策》第 13 章说的那个「剪枝」—— 它把搜索算法整块指到了本板块,这一章是它 🔗 表里点名的落点。


♟ 一、动机:证明一步棋坏,只要一个理由

讲义在讲算法之前先问了两个问题:

Q1:为什么「皇后到 G5」对黑方是步坏棋? Q2:你为了回答 Q1,考虑了白方的几种应招?

答案是:一种就够。只要看到一个足够可怕的回应,这步棋就废了 —— 白方另外三十几种走法长什么样,你不需要知道。

⭐⭐ 讲义把它写成一句对仗: 要证明一步棋坏,只需要考虑一个(好的)应招;要证明一步棋好,得看遍所有应招。 α-β 剪枝的全部内容就是这一句,剩下的都是记账。


🔤 二、α 和 β 各是什么

含义 是什么 方向
$\alpha$ 在当前路径之外,MAX 方已经确保能拿到的最好值 根值的下界 只增不减
$\beta$ 在当前路径之外,MIN 方已经确保能把 MAX 压到的最好值 根值的上界 只减不增

每个节点都带着一个窗口 $[\alpha, \beta]$ 往下走。只有值落进这个窗口,才可能改变根的决定。

⚠️ 最常见的误解:$\alpha$ 和 $\beta$ 不是当前节点的值,而是从祖先一路传下来的约束 —— 讲义把重点全押在一个短语上:它们都是 off the current path(不在当前这条路上)找到的最好值。

⭐ 如果当前节点的值超过了 $\beta$,说明它「好得不真实」—— 对方根本不会让我走到这儿,于是剩下的孩子直接剪掉。


✂ 三、什么时候能剪,为什么剪掉是安全的

规则:MIN 节点上 $\beta \le \alpha$ 就剪;MAX 节点上 $\alpha \ge \beta$ 就剪。剪掉的是这个节点剩下还没看的孩子

为什么安全(以 MIN 节点为例,三句话):

  1. 这个 MIN 节点上已经看到某个孩子的值 $v$,而且 $v \le \alpha$
  2. MIN 层只会取更小的,所以此节点最终值一定 $\le v \le \alpha$
  3. 而 $\alpha$ 是 MAX 在别的分支上已经拿到手的保底 —— 这一支给不出更好的,MAX 绝不会选它,剩下的孩子改不了根的决定

⭐ 讲义原话:α-β pruning is guaranteed to give the same result as minimax.

⚠️ 这句话要精确理解:根的值和选出的走法一字不差,但被剪过的内部节点,值不再精确,它变成了一个界 (比如只知道「$\le 2$」,不知道到底是 2 还是 $-7$)。 ⭐ 这不是缺陷,第 12 章会看到 TreeStrap 这类算法专门拿这些界当训练目标

讲义给的伪代码:

function alphabeta(node, depth, α, β)
    if node is terminal or depth = 0 { return heuristic value of node }
    if we are to play at node                    // MAX
        foreach child of node
            α = max(α, alphabeta(child, depth−1, α, β))
            if α ≥ β { return α }                // β 截断
        return α
    else                                          // MIN
        foreach child of node
            β = min(β, alphabeta(child, depth−1, α, β))
            if β ≤ α { return β }                // α 截断
        return β

🧮 四、在讲义那棵树上完整手算

还是第 10 章那棵树:根是 MAX,三个 MIN 孩子 $B$、$C$、$D$,叶子 $[3,12,8]$、$[2,4,6]$、$[14,5,2]$,从左往右深度优先

α-β 剪枝:9 个叶子只看了 7 个,根值一点没变MAXMIN终局3根:α 被左支抬到 33≤22进来时 α = −∞进来时 α = 3进来时 α = 3剪掉:这两个叶子根本没看31282461452中间那一支只看了第一个叶子 2 就够了:它已经比左支的保底 3 还差
虚线是被剪掉的分支。中间支 C 的第一个叶子给出 2,MIN 只会让 C 变得更小,而根已经有 3 打底 —— 所以 C 不可能被选中,剩下的 4 和 6 连读都没读。

一步一步($\alpha$ 是根一路传下来的下界):

在哪 发生了什么 剪了吗
1 $B$(MIN),窗口 $[-\infty, +\infty]$ 叶 3 → $\beta=3$;叶 12、叶 8 都不更小。$B = 3$ 没有($\alpha = -\infty$,剪不动)
2 回到根 $\alpha = \max(-\infty, 3) = 3$ ——
3 $C$(MIN),窗口 $[\mathbf{3}, +\infty]$ 叶 2 → $\beta = 2$。⭐ $\beta = 2 \le \alpha = 3$ 剪掉叶 4 和叶 6,返回「$C \le 2$」
4 $D$(MIN),窗口 $[3, +\infty]$ 叶 14 → $\beta=14>3$;叶 5 → $\beta=5>3$;叶 2 → $\beta=2\le3$ 触发了,但 2 已是最后一个孩子,白搭
5 $\max(3,\ \le 2,\ 2) = \mathbf{3}$,走 $B$ 和第 10 章一模一样

:9 个叶子看了 7 个。⚠️ 第 4 步值得盯一眼:$D$ 也触发了截断条件,但触发得太晚(最后一个孩子),收益是零。 ⭐ 剪枝省多少,完全取决于「该剪的分支排在第几个」 —— 这就是下一节。

🔨 把开关拨一下,数给你看

# 同一棵树,prune 开关一拨,数一数少看了几个叶子
TREE = [[3, 12, 8], [2, 4, 6], [14, 5, 2]]

def search(node, is_max, alpha, beta, seen, prune):
    if isinstance(node, int):
        seen.append(node)                       # 叶子:记一笔「看过了」
        return node
    if is_max:
        v = float("-inf")
        for c in node:
            v = max(v, search(c, False, alpha, beta, seen, prune))
            alpha = max(alpha, v)               # ⭐ α = 我方在别处已经保底的值
            if prune and alpha >= beta:         # ⭐ β 截断:对方不会放我走到这儿
                break
        return v
    v = float("inf")
    for c in node:
        v = min(v, search(c, True, alpha, beta, seen, prune))
        beta = min(beta, v)                     # ⭐ β = 对方在别处已经把我压到的值
        if prune and beta <= alpha:             # ⭐ α 截断:这一支不可能被我方选中
            break
    return v

INF = float("inf")
for prune in (False, True):
    seen = []
    v = search(TREE, True, -INF, INF, seen, prune)
    print("%s 根值=%d  看了 %d 个叶子:%s" %
          ("α-β  " if prune else "不剪枝", v, len(seen), seen))

实跑输出:

不剪枝 根值=3  看了 9 个叶子:[3, 12, 8, 2, 4, 6, 14, 5, 2]
α-β   根值=3  看了 7 个叶子:[3, 12, 8, 2, 14, 5, 2]

根值都是 3。 这不是运气 —— 是第三节那三句话证明过的。


📉 五、节点序:这不是常数优化,是指数砍掉一半

走法顺序 复杂度
最坏(好棋排最后) $O(b^m)$ —— 一个都剪不掉,退回原样
随机 大约 $O(b^{3m/4})$
⭐⭐ 完美(好棋排最前) $O(b^{m/2}) = O\big((\sqrt b\,)^m\big)$

国际象棋 $b \approx 35$,$\sqrt{35} \approx 5.9$ —— ⭐ 完美排序等于把分支因子从 35 压到 6。 搜到 8 层要看的叶子数:不剪枝 $35^8 \approx 2.25\times10^{12}$,随机序 $\approx 35^6 \approx 1.84\times10^{9}$, 完美序 $35^4 \approx 1.50\times10^{6}$ —— 少了 6 个数量级。

⭐⭐ 但真正该记住的是换算成深度的那句话(讲义原文):

α-β 可以搜到普通 Minimax 两倍的深度。 讲义补了一句:搜索深度从 6 提到 12,能把一个很弱的棋手变成一个相当强的棋手。

这笔账自己算一遍:同样的节点预算 $N$,不剪枝搜 $\log_b N$ 层,完美序搜 $\log_{\sqrt b} N = 2\log_b N$ 层 —— 正好两倍


🔧 六、怎么把好走法排到前面

排序不需要准,只需要比随机好。工程上这几条加起来就够用:

手段 做法
吃子优先 先试「用小子吃大子」(MVV-LVA),这类走法最容易造成截断
⭐ 迭代加深 先搜深度 1、2、3……用上一层的最佳走法给下一层排序
杀手走法 在某处造成过截断的走法记下来,在同深度的别处先试它(「最佳应招」同理,记的是造成截断那步的前一步)
置换表 同一局面能从不同走法顺序到达;哈希表存下已算过的值,撞上直接取

⚠️ 迭代加深看着很浪费 —— 深度 1 到 $d-1$ 全白搜了一遍。其实不然:节点数按 $b$ 指数增长, 前面所有层加起来也只比最后一层多约 $\dfrac{1}{b-1}$,$b = 35$ 时就是 3%。 用 3% 换一个好排序,而排序的收益是指数级的 —— 所以现代引擎全都这么干。


🔗 这一章连到哪里

去哪 为什么
10-博弈树与Minimax.html 同一棵树那边看了 9 个叶子、这边 7 个,根值都是 3 —— 对着读才看得出剪枝省在哪
../博弈论与集体决策/13-零和与极大极小.html ⭐⭐ 那一章写着「零和结论换成极小化极大搜索 + 剪枝」,并把搜索算法本身指到本板块 10–12 章 —— 本章就是它 🔗 表里点名的那个落点
12-评估函数与棋类AI.html 剪枝让你搜得更深,但总要停;停下那一刻拿什么给局面打分,是下一章的问题
../机器学习与深度学习基础/04-决策树与集成学习.html ⚠️ 决策树里也有「剪枝」,但那是为了不过拟合而砍掉分支、答案会变;α-β 砍掉的分支保证不影响答案。同名不同事

✅ 检查点

  1. 讲义用「皇后到 G5」问的那两个问题,想说明什么?把结论写成一句对仗的话。
  2. $\alpha$ 和 $\beta$ 各代表什么?为什么说它们「和当前节点没有直接关系」?
  3. MIN 节点上什么时候剪?用三句话说清为什么剪掉是安全的。
  4. α-β 和 Minimax 的结果「完全一样」这句话,精确说应该怎么讲?
  5. 手算:树是 $[3,12,8]$、$[2,4,6]$、$[14,5,2]$,从左往右深度优先。哪几个叶子被剪掉了?根值是多少?一共看了几个叶子?
  6. 第 4 步里 $D$ 也触发了截断条件,为什么一点收益都没有?这说明了什么?
  7. 完美排序下复杂度是多少?换算成「深度」是什么结论?$b = 35$ 时 $\sqrt b$ 大约是几?
  8. 迭代加深要把浅层重搜一遍,为什么这不算浪费?
👀 答案
  1. 想说明一种应招就够:只要看到一个足够可怕的回应,这步棋就废了。⭐⭐ 对仗句 —— 要证明一步棋坏,只需要考虑一个(好的)应招;要证明一步棋好,得看遍所有应招。
  2. $\alpha$ = 在当前路径之外、MAX 方已确保拿到的最好值(根值的下界,只增);$\beta$ = 在当前路径之外、MIN 方已确保把 MAX 压到的值(上界,只减)。⚠️ 它们记录的是「树的别处发生过什么」,是从祖先传下来的约束
  3. $\beta \le \alpha$ 时剪掉剩下没看的孩子。① 已看到某孩子值 $v \le \alpha$;② MIN 只会取更小,此节点最终值 $\le v \le \alpha$;③ $\alpha$ 是 MAX 在别处已到手的保底,这一支给不出更好的,MAX 绝不会选 —— 剩下的孩子改不了根的决定。
  4. 根值和选出的走法一字不差;但被剪过的内部节点值不再精确,退化成一个界(只知道「$\le 2$」)。⭐ 不是缺陷 —— TreeStrap 专门拿这些界当训练目标。
  5. 被剪的是 $C$ 的第二、第三个叶子 4 和 6。根值 $= \max(3, \le 2, 2) = \mathbf{3}$,走最左支,一共看了 7 个叶子。
  6. 因为它触发在最后一个孩子上,后面没东西可剪了。⭐ 说明剪枝省多少全看该剪的分支排在第几个 —— 所以走法排序是 α-β 的胜负手。
  7. $O(b^{m/2}) = O((\sqrt b)^m)$。换算成深度:预算 $N$ 时不剪枝搜 $\log_b N$ 层,完美序搜 $\log_{\sqrt b} N = 2\log_b N$ 层,正好两倍深。$b=35$ 时 $\sqrt{35}\approx5.9$,分支因子从 35 压到 6;深度从 6 提到 12 能把很弱的棋手变成相当强的棋手。
  8. 节点数按 $b$ 指数增长,前面所有层加起来只比最后一层多约 $1/(b-1)$,$b=35$ 时是 3% —— 3% 换一个指数级收益的排序。

🛑 可以停在这里

走神救援

α-β 剪枝的全部思想是讲义那句对仗:要证明一步棋坏只需要一个(好的)应招,要证明一步棋好得看遍所有应招。做法是搜索时随身带一个窗口 $[\alpha,\beta]$:$\alpha$ 是在当前路径之外 MAX 方已确保拿到的最好值(根值下界,只增),$\beta$ 是 MIN 方已确保把 MAX 压到的值(上界,只减)。⚠️ 它们不是当前节点的值,是从祖先传下来、记录「树的别处发生过什么」的约束。规则:MIN 节点 $\beta \le \alpha$ 就剪,MAX 节点 $\alpha \ge \beta$ 就剪。安全性三句:已看到某孩子值 $v \le \alpha$ → MIN 只会更小、此节点最终 $\le \alpha$ → 而 $\alpha$ 是 MAX 在别处已到手的保底,这一支绝不会被选。⭐ 讲义那句「保证给出和 minimax 一样的结果」要精确读:根值和走法一字不差,但被剪的内部节点值退化成一个界(如「$\le 2$」)—— 这不是缺陷,第 12 章的 TreeStrap 专门拿这些界当训练目标。手算($[3,12,8]/[2,4,6]/[14,5,2]$,从左往右):$B$ 三个叶子全看得 3,根 $\alpha$ 抬到 3;$C$ 只看第一个叶子 2 就有 $\beta=2\le\alpha=3$,✂ 叶 4 和叶 6 直接不看,返回「$\le 2$」;$D$ 三个叶子全看得 2(最后一个也触发截断,但太晚,收益为零);根 $=\max(3,\le2,2)=3$。9 个叶子看了 7 个,代码把 prune 开关一拨就能验。⭐ $D$ 那一下说明:剪枝省多少全看该剪的分支排在第几个。所以节点序是胜负手:最坏序 $O(b^m)$ 一个都剪不掉,随机序约 $O(b^{3m/4})$,⭐⭐ 完美序 $O(b^{m/2})=O((\sqrt b)^m)$。$b=35$ 时 $\sqrt{35}\approx5.9$,分支因子从 35 压到 6;搜 8 层的叶子数从 $2.25\times10^{12}$ 降到 $1.50\times10^{6}$,少 6 个数量级。换算成深度就是讲义那句:同样时间能搜两倍深($\log_{\sqrt b}N = 2\log_b N$),而深度从 6 提到 12 能把一个很弱的棋手变成相当强的棋手。排序手段:吃子优先、⭐ 迭代加深(用上一层的最佳走法给下一层排序)、杀手走法、最佳应招、置换表。⚠️ 迭代加深看着浪费,其实前面所有层加起来只比最后一层多约 $1/(b-1)$,$b=35$ 时是 3% —— 3% 换一个指数级收益的排序。

下一节 👉 12-评估函数与棋类AI.html

打卡记录保存在你的浏览器里,首页能看到总进度