工作台课程

CS221 · 人工智能:原理与技术

Lecture 10 · 博弈 I:Minimax 与 α-β 剪枝

Lecture 10 · 博弈 I:Minimax 与 α-β 剪枝

CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Oct 22 · 💻 games.py


从 MDP 到博弈

上周(Lecture 7 · 马尔可夫决策过程Lecture 8 · 强化学习:智能体在随机环境中最大化期望效用。环境的”随机”来自自然因素——掷骰子、交通状况;环境的转移概率 $T$ 与奖励 $R$ 要么已知(可用价值迭代直接求解),要么未知但可以通过交互学习(Q-learning)。关键在于:环境没有”意图”,它的随机性不针对你。

本周(博弈,games):智能体仍然想最大化效用,但”世界的另一半”换成了一个对手(opponent)。这带来两个本质变化:其一,对手的策略是未知的——我们手里没有它的”转移概率表”;其二,对手可能在故意跟你作对——它不是被动的随机源,而是主动的对抗者。环境从”被动的随机”变成了”主动的对抗”。

动机例子:一棵小游戏树(下文 Game1),你在根节点先手,你该选哪个动作?

提示

你的最优策略取决于对手怎么下。如果对手随机乱走,和对手拼命想赢你,你算出来的最优动作完全不同(本讲会在同一棵树上算出两个不同答案:expectimax 选 C,minimax 选 B)。整讲的主线就是一句话:“对对手做什么假设”决定了你算出来的策略。

维度 MDP 博弈(Games)
谁在控制”另一半” 环境(自然) 对手(另一个智能体)
那一半的行为 随机、已知/可学 对抗、未知
目标 最大化期望效用 最大化效用(对手想最小化它)
类比算法 价值迭代 expectimax / minimax

1. 博弈的形式化(Modeling)

本讲聚焦两人零和博弈(two-player zero-sum games)

  • 两个玩家agent(我们)和 opp(对手 opponent);
  • 零和(zero-sum):$\text{utility}_{\text{agent}}(s) = -\,\text{utility}_{\text{opp}}(s)$——一方赢多少,另一方就输多少,蛋糕大小固定。因此整个博弈只需要一个效用函数(站在 agent 的角度),对手的目标自动就是最小化它。

提示

“零和”是一个强而好用的简化:它把”两个玩家各有所图”压缩成”一个数值、两种态度”(agent 想抬高、opp 想压低)。这正是后面 minimax 里 $\max$ 与 $\min$ 可以作用在同一个 $V$ 上的原因。非零和博弈(如囚徒困境)需要每人一张收益表,留到 Lecture 11 · 博弈 II:TD 学习与同时博弈

1.1 Game 的接口

一个博弈由下面五个部分定义(对比搜索/MDP:多了一个 player,因为不同状态由不同玩家控制):

class Game:
    def start_state(self) -> Any:
        """游戏从哪里开始"""
        raise NotImplementedError

    def successors(self, state: Any) -> dict[str, Any]:
        """后继:动作 → 下一状态(注意这里是 dict,不是列表)"""
        raise NotImplementedError

    def player(self, state: Any) -> str:
        """在 state 该谁行动?('agent' 或 'opp')"""
        raise NotImplementedError

    def is_end(self, state: Any) -> bool:
        """游戏结束了吗?"""
        raise NotImplementedError

    def utility(self, state: Any) -> float:
        """游戏的效用(站在 agent 的角度)"""
        raise NotImplementedError

博弈的两个关键特征(源码原话):

  • 所有效用都在终止状态——utility(s) 只在 is_end(s) 时才有定义。这是稀疏奖励(sparse reward):中途每一步都没有奖励信号,只有走到底才知道输赢。这一点让”学会评估中间局面”(下一讲的 TD 学习)成为必须解决的问题。
  • 不同状态由不同玩家控制——player(s) 告诉你这一层轮到谁行动。这是博弈区别于 MDP 的本质:MDP 里每个状态都是 agent 在决策,博弈里控制权在玩家之间交替。

注意

注意和 MDP 接口的三处不同:① successors 返回 dict[动作, 下一状态] 而非带概率的后继列表——博弈中”我落子后棋盘变成什么”是确定性的,随机性(如骰子)留给后面的机会节点单独处理;② 多了 player(s);③ 用一次性的 utility 而非逐步的 reward,且只在终点有值。

1.2 例子 1:一棵小游戏树

Game1 是一棵深度为 2 的树:根节点 agent 先选 A/B/C,然后对手在每个分支里选 1/2,叶子给出效用。

class Game1(Game):
    def start_state(self) -> Any:
        return "root"

    def successors(self, state: str) -> dict[str, str]:
        # state -> action -> next state
        mapping = {
            "root": {"A": "A", "B": "B", "C": "C"},
            "A": {"1": "A1", "2": "A2"},
            "B": {"1": "B1", "2": "B2"},
            "C": {"1": "C1", "2": "C2"},
        }
        return mapping[state]

    def player(self, state: Any) -> str:
        if state == "root":
            return "agent"          # 根节点:我们先手
        if state in ["A", "B", "C"]:
            return "opp"            # 第二层:对手行动
        raise ValueError(f"Invalid state: {state}")

    def is_end(self, state: Any) -> bool:
        return state in ["A1", "A2", "B1", "B2", "C1", "C2"]

    def utility(self, state: Any) -> float:
        utilities = {
            "A1": -50, "A2": 50,
            "B1": 1,   "B2": 3,
            "C1": -5,  "C2": 15,
        }
        return utilities[state]

游戏树长这样(🟩 agent 层、🟦 opp 层、🟧 叶子效用):

graph TD
    R["root<br/>agent(max)"]
    R -->|A| A["A<br/>opp(min)"]
    R -->|B| B["B<br/>opp(min)"]
    R -->|C| C["C<br/>opp(min)"]
    A -->|1| A1["A1 = -50"]
    A -->|2| A2["A2 = 50"]
    B -->|1| B1["B1 = 1"]
    B -->|2| B2["B2 = 3"]
    C -->|1| C1["C1 = -5"]
    C -->|2| C2["C2 = 15"]

    style R fill:#c8e6c9,stroke:#2e7d32
    style A fill:#e1f5fe,stroke:#0277bd
    style B fill:#e1f5fe,stroke:#0277bd
    style C fill:#e1f5fe,stroke:#0277bd
    style A1 fill:#ffe0b2,stroke:#e65100
    style A2 fill:#ffe0b2,stroke:#e65100
    style B1 fill:#ffe0b2,stroke:#e65100
    style B2 fill:#ffe0b2,stroke:#e65100
    style C1 fill:#ffe0b2,stroke:#e65100
    style C2 fill:#ffe0b2,stroke:#e65100

1.3 例子 2:减半游戏(Halving Game)

一个能真正”玩到底”的博弈:两人轮流对一个数 $n$ 做减一(decrement)减半(half,整数除以 2)。胜负规则(对照代码确认):轮到某个玩家时 $n$ 已经是 0,该玩家获胜——换个说法,亲手把 $n$ 变成 0 的一方输,因为把 0 留给对手就是把胜利送给对手。

注意状态里必须带上 player——同一个数 $n$,轮到我还是轮到对手,输赢完全相反;只记 $n$ 不足以判断局面。

@dataclass(frozen=True)
class HalvingState:
    n: int          # 当前的数
    player: str     # 轮到谁(状态必须带上,否则无法判断输赢)

class HalvingGame(Game):
    def __init__(self, n: int):
        self.n = n

    def start_state(self) -> Any:
        return HalvingState(n=self.n, player="agent")

    def successors(self, state: HalvingState) -> dict[str, Any]:
        next_player = "opp" if state.player == "agent" else "agent"
        return {
            "decrement": HalvingState(n=state.n - 1, player=next_player),
            "half":      HalvingState(n=state.n // 2, player=next_player),
        }

    def player(self, state: HalvingState) -> str:
        return state.player

    def is_end(self, state: HalvingState) -> bool:
        return state.n == 0

    def utility(self, state: HalvingState) -> float:
        assert state.n == 0
        # 轮到谁时数已经是 0,谁就赢
        return +1 if state.player == "agent" else -1

注意

别把规则记反:不是”把对手逼到 0 的人赢”,而是”面对 0 的人赢“。走 $1 \to 0$ 的那一步是送分——后继状态轮到对手且 $n=0$,效用判对手赢。§2.3 的手算例子会用到这一点。

1.4 策略:确定性 vs 随机

策略(policy) 是玩家 $p$ 在状态 $s$ 的决策规则,有两种:

类型 记号 含义
确定性(deterministic) $\pi_p(s)$ 玩家 $p$ 在状态 $s$ 确定采取的动作
随机(stochastic) $\pi_p(a\mid s)$ 玩家 $p$ 在 $s$ 按概率分布选动作 $a$

源码统一用随机策略表达(确定性策略只是把某个动作的概率设成 1.0 的特例),策略返回一个 {动作: 概率} 字典。simulate 让轮到的玩家各按自己的策略采样动作,一路走到终局:

Policy = Callable[[Any], Any]   # state -> {action: prob}

def simulate(game: Game, policies: dict[str, Policy]) -> Rollout:
    """从起始状态用各自的策略模拟一整局。"""
    state = game.start_state()
    steps = []
    while not game.is_end(state):
        player = game.player(state)         # 轮到谁
        actions = policies[player](state)   # {action: prob}
        action = sample_dict(actions)       # 按概率采样一个动作
        state = game.successors(state)[action]
        steps.append(Step(action=action, state=state))
    return Rollout(steps=steps, utility=game.utility(state))  # 走到底看谁赢

2. 递归定义一系列结果

本讲的核心骨架:同一个递归框架,改变”每类玩家节点如何汇聚子节点的价值”,就得到四种不同的价值

结果 agent 节点 opp 节点 类比 MDP
game evaluation 按 $\pi_{\text{agent}}$ 加权 按 $\pi_{\text{opp}}$ 加权 策略评估
expectimax $\max$ 按 $\pi_{\text{opp}}$ 加权(期望) 价值迭代
minimax $\max$ $\min$(对抗) 无对应
expectiminimax $\max$ $\min$ + 机会节点求期望 无对应

提示

这四种递归其实在回答同一个问题的四个版本:”这个局面值多少分?”——答案取决于你对每一层的控制者做什么假设:自己人取 $\max$(挑最好的),已知的随机方取期望(按分布平均),对抗方取 $\min$(做最坏打算)。把”每层用什么算子”想清楚,任何新变体(多玩家、连走两步、骰子插队)都能立刻写出对应递归。

2.1 Game Evaluation:给定双方策略,求价值

问题:双方策略都固定时,这局游戏的价值(value)——即所有可能对局(rollout)上的期望效用——是多少?

蒙特卡洛法可以模拟很多局取平均,但有方差且慢。用递归可以精确求出:

$$V_{\text{eval}}(s)=\begin{cases}\text{utility}(s), & \text{若 } \mathrm{isEnd}(s)\\[4pt] \displaystyle\sum_{a}\pi_{p}(a\mid s)\,V_{\text{eval}}\big(\mathrm{succ}(s,a)\big), & \text{否则,其中 } p=\mathrm{player}(s)\end{cases}$$

符号说明:$\mathrm{succ}(s,a)$ 是在 $s$ 执行动作 $a$ 后到达的(确定的)下一状态;$\pi_p(a\mid s)$ 是轮到的玩家 $p$ 选动作 $a$ 的概率。不管是 agent 还是 opp 的节点,都按该玩家自己的策略概率加权——因为两边策略都已知,没有任何”选择”要做,只是算一个期望。

def V_eval(game: Game, policies: dict[str, Policy], state: Any) -> float:
    """给定双方策略,返回博弈的价值(期望效用)。"""
    if game.is_end(state):
        return game.utility(state)

    # 轮到谁,就用谁的策略
    player = game.player(state)
    policy = policies[player]

    value = 0
    for action, prob in policy(state).items():
        next_state = game.successors(state)[action]
        value += prob * V_eval(game, policies, next_state)   # 按策略概率加权

    return value

说明

这与 Lecture 7 · 马尔可夫决策过程策略评估(policy evaluation)完全对应:都是”给定策略求期望价值”。递归精确,但一般需要展开整棵树,时间随深度指数增长

2.2 Expectimax:对手策略已知,求 agent 最优策略

问题:不再固定 agent 的策略,而是求 agent 的最优策略;但假设对手的策略 $\pi_{\text{opp}}$ 已知且固定(比如已知对手在动作 1/2 之间各以 0.5 随机选)。

$$V_{\text{exptmax}}(s)=\begin{cases}\text{utility}(s), & \text{若 } \mathrm{isEnd}(s)\\[4pt] \displaystyle\max_{a} V_{\text{exptmax}}\big(\mathrm{succ}(s,a)\big), & \text{若 } \mathrm{player}(s)=\text{agent}\\[4pt] \displaystyle\sum_{a}\pi_{\text{opp}}(a\mid s)\,V_{\text{exptmax}}\big(\mathrm{succ}(s,a)\big), & \text{若 } \mathrm{player}(s)=\text{opp}\end{cases}$$

agent 节点取 $\max$(在自己能控制的层挑最好的动作),opp 节点取期望(对手照它已知的分布走,我们对其随机性求平均)。

def V_exptmax(game: Game, opp_policy: Policy, state: Any) -> float:
    if game.is_end(state):
        return game.utility(state)

    player = game.player(state)

    if player == "agent":
        # 选让效用最大的动作
        next_states = list(game.successors(state).values())
        values = [V_exptmax(game, opp_policy, s) for s in next_states]
        return np.max(values)

    elif player == "opp":
        # 对手照它已知的(随机)策略走 → 求期望
        successors = game.successors(state)          # action -> next state
        values = [prob * V_exptmax(game, opp_policy, successors[a])
                  for a, prob in opp_policy(state).items()]
        return np.sum(values)

    else:
        raise ValueError(f"Invalid player: {player}")

例子

若对手随机(1/2 各 0.5),三个分支的期望价值分别是:
$$\text{A}: \tfrac{-50+50}{2}=0,\qquad \text{B}: \tfrac{1+3}{2}=2,\qquad \text{C}: \tfrac{-5+15}{2}=5$$
所以 expectimax 的最优动作是 C——既然对手不针对你,就该去期望收益最高的分支赌一把。结构上这正是 Lecture 7 · 马尔可夫决策过程 里的价值迭代:agent 侧取 $\max$,”环境”侧求期望;对手退化成了一个已知的随机环境。

2.3 Minimax:对手对抗,取 min

问题的关键:博弈的要害恰恰是我们不知道对手策略!expectimax 假设对手策略已知,太乐观——万一对手不是随机的,而是个高手呢?

说明

假设对手会下他所能下的最强的一手——即对手在每个节点都最小化你的效用。在这个最坏假设下求自己的最优策略。

$$V_{\text{minmax}}(s)=\begin{cases}\text{utility}(s), & \text{若 } \mathrm{isEnd}(s)\\[4pt] \displaystyle\max_{a} V_{\text{minmax}}\big(\mathrm{succ}(s,a)\big), & \text{若 } \mathrm{player}(s)=\text{agent}\\[4pt] \displaystyle\min_{a} V_{\text{minmax}}\big(\mathrm{succ}(s,a)\big), & \text{若 } \mathrm{player}(s)=\text{opp}\end{cases}$$

agent 层取 $\max$,opp 层取 $\min$——不需要事先给定任何策略,双方都被假设为完美对抗,策略从递归本身”涌现”出来。

def V_minmax(game: Game, state: Any) -> tuple[float, Action]:
    """返回博弈价值 + 最优动作。"""
    if game.is_end(state):
        return game.utility(state), None

    player = game.player(state)

    # 对所有后继递归求值
    successors = game.successors(state)
    values = {action: V_minmax(game, next_state)[0]
              for action, next_state in successors.items()}

    if player == "agent":       # agent 最大化效用
        action, value = max(values.items(), key=lambda x: x[1])
    elif player == "opp":       # 对手最小化效用
        action, value = min(values.items(), key=lambda x: x[1])
    else:
        raise ValueError(f"Invalid player: {player}")

    return value, action

minimax 在 Game1 上的计算(从叶子往上,红色节点是取 $\min$ 后的值):

graph BT
    A1["A1=-50"] --> A["A: min = -50"]
    A2["A2=50"] --> A
    B1["B1=1"] --> B["B: min = 1"]
    B2["B2=3"] --> B
    C1["C1=-5"] --> C["C: min = -5"]
    C2["C2=15"] --> C
    A --> R["root: max(-50,1,-5) = 1<br/>→ 选 B"]
    B --> R
    C --> R

    style A1 fill:#e1f5fe,stroke:#0277bd
    style A2 fill:#e1f5fe,stroke:#0277bd
    style B1 fill:#e1f5fe,stroke:#0277bd
    style B2 fill:#e1f5fe,stroke:#0277bd
    style C1 fill:#e1f5fe,stroke:#0277bd
    style C2 fill:#e1f5fe,stroke:#0277bd
    style A fill:#ffcdd2,stroke:#c62828
    style B fill:#ffcdd2,stroke:#c62828
    style C fill:#ffcdd2,stroke:#c62828
    style R fill:#ffe0b2,stroke:#e65100

提示

对比 §2.2:同一棵树,expectimax 选 C(赌对手随机,追求期望 5),minimax 选 B(防对手最优,锁定保底 1)。C 分支虽然期望高,但最坏情况是 $-5$;B 分支哪怕对手完美应对也能拿到 $+1$。对对手的假设不同,最优动作就不同。 minimax 在 MDP 里没有对应物——因为 MDP 的”另一半”从不对抗你。

例子

轮到 agent,$n=3$,两个选择:

  • decrement:$3\to 2$,轮到对手。对手在 $n=2$ 无论 decrement 还是 half 都得到 $n=1$ 并把它留给你;而你在 $n=1$ 时无论怎么走都会亲手把 $n$ 变成 0(送对手赢)。所以 decrement 分支的 minimax 值是 $-1$。
  • half:$3\to 1$,轮到对手。对手在 $n=1$ 无论怎么走都亲手把 $n$ 变成 0——对手必输。这个分支的 minimax 值是 $+1$。

所以 $V_{\text{minmax}}(3)=\max(-1,+1)=+1$,最优动作是 half。同样的手算可验证:$n=1,4$ 是必败起点(价值 $-1$),$n=2,3,5$ 是必胜起点(价值 $+1$)。

减半游戏里的碾压:让 agent 用 minimax 策略(每步取 $V_{\text{minmax}}$ 给出的最优动作、确定性地下),对手用随机策略,两者对打时 minimax 把随机策略打得落花流水。再逐个初始 $n$ 求 minimax 价值:

game = HalvingGame(n=11)
results = {n: V_minmax(game, HalvingState(n=n, player="agent"))
           for n in range(1, 12)}
  • 价值 $=+1$:无论对手怎么走,agent 保证赢(存在必胜路线);
  • 价值 $=-1$:只要对手下得最优,agent 必输——但对手若失误,agent 仍可能翻盘。

说明

双方都按 minimax 最优下叫完美对弈(perfect play);若完美对弈下的结果已被证明,就说该游戏被解出(solved)

  • 强解出(从任意局面都知道结果与最优走法):井字棋、Nim、四子棋;
  • 弱解出(只从初始局面知道结果):西洋跳棋(checkers,平局)、黑白棋;
  • 未解出:国际象棋、围棋——注意”计算机棋力超人类”与”游戏被解出”是两回事,前者是工程近似,后者是数学证明。

2.4 Expectiminimax:既有随机又有对抗

现实游戏里常常同时有对手(对抗)和骰子(随机)——比如西洋双陆棋(backgammon)。此时把掷骰子建模为第三类”玩家”——机会节点(chance node),游戏树里出现三类节点:agent($\max$)、opp($\min$)、chance(求期望):

$$V(s)=\begin{cases}\text{utility}(s), & \mathrm{isEnd}(s)\\[2pt] \max_{a} V\big(\mathrm{succ}(s,a)\big), & \mathrm{player}(s)=\text{agent}\\[2pt] \min_{a} V\big(\mathrm{succ}(s,a)\big), & \mathrm{player}(s)=\text{opp}\\[2pt] \sum_{a} P(a\mid s)\,V\big(\mathrm{succ}(s,a)\big), & \mathrm{player}(s)=\text{chance}\end{cases}$$

其中 $P(a\mid s)$ 是机会节点的已知分布(如骰子各面 $1/6$)——它不是谁的”策略”,而是物理规律,所以永远取期望。

说明

一旦想通”每类玩家节点用什么算子汇聚子节点”,这个框架可以任意扩展:多于两个玩家、某玩家连走两步、由状态决定下一个行动者……只要把递归写成一般形式即可。但也有游戏树覆盖不了的情形:

  • 不完美信息(imperfect information)游戏:如扑克,看不到对手的牌,状态本身对玩家不完全可见;
  • 非零和(non-zero-sum)游戏:如囚徒困境,双方利益不完全对立 → Lecture 11 · 博弈 II:TD 学习与同时博弈
  • 非回合制游戏:如石头剪刀布,双方同时出招,没有”先后”可言 → 同样见下一讲。

3. Minimax 的性质:对谁最优?

计算不同的递归会产生不同的策略。记 $V(\pi_{\text{agent}}, \pi_{\text{opp}})$ 为”agent 用 $\pi_{\text{agent}}$、对手用 $\pi_{\text{opp}}$ 对打时的博弈价值”(按 §2.1 的 $V_{\text{eval}}$ 算出)。几个角色:

  • $\pi_{\max}, \pi_{\min}$:minimax 递归同时给出的 agent 策略与对手策略;
  • $\pi_{7}$:某个已知的固定对手策略(讲义用编号 7 泛指”随便某一个策略”,例如均匀随机);
  • $\pi_{\text{exptmax}(7)}$:假设对手就是 $\pi_7$ 时,expectimax 算出的 agent 最优应对。

注意

每个”最优”策略都内建了对另一方怎么下的假设。谈最优,永远要追问一句:相对于谁最优(optimality with respect to what)? 脱离对手模型谈”最优策略”没有意义。

三条性质(把不同策略两两对打):

性质 不等式 含义
性质 1 $V(\pi_{\text{exptmax}(7)}, \pi_{\min}) \le V(\pi_{\max}, \pi_{\min})$ 面对 $\pi_{\min}$,$\pi_{\max}$ 是最好的 agent 策略;针对错误对手模型优化会吃亏
性质 2 $V(\pi_{\max}, \pi_{\min}) \le V(\pi_{\max}, \pi_{7})$ 面对 $\pi_{\max}$,$\pi_{\min}$ 是最好的对手策略;换任何别的对手只会让 agent 拿得更多
性质 3 $V(\pi_{\max}, \pi_{7}) \le V(\pi_{\text{exptmax}(7)}, \pi_{7})$ 确实知道对手是 $\pi_7$,$\pi_{\text{exptmax}(7)}$ 比 minimax 更好

把三条串成一条链:

$$V(\pi_{\text{exptmax}(7)}, \pi_{\min}) \;\le\; V(\pi_{\max}, \pi_{\min}) \;\le\; V(\pi_{\max}, \pi_{7}) \;\le\; V(\pi_{\text{exptmax}(7)}, \pi_{7})$$

提示

以 minimax 价值 $V(\pi_{\max}, \pi_{\min})$ 为保底下界:对手若非最优,agent 只会更好(性质 2);若还知道对手模型,改用 expectimax 能再更好(性质 3);但如果对手其实很强而你按弱对手模型优化,就会跌破保底(性质 1 的左端)。

所以 minimax 同时给你两样东西:① 相对于最坏对手的最优性;② 面对任意(未知)对手的下界保证——若 minimax 价值 $=+1$,则无论对手怎么下 agent 都保证赢。代价是:如果你了解对手的弱点,minimax 会偏保守——知己知彼时,expectimax 能打得更好(”You can do better than minimax if you know your opponent!”)。


4. α-β 剪枝:加速 minimax(精确)

回到最基本的 minimax——它要展开整棵游戏树,时间是 $O(b^d)$($b$ 为分支因子,即每层平均可选动作数;$d$ 为树深,即总步数),一般是指数时间。怎么加速?

说明

不去访问那些已经能断定不可能影响最终答案的状态。关键性质:它不改变 minimax 的结果,是精确加速——被剪掉的子树被证明不可能被最优路径经过。

4.1 分支限界(branch and bound)的直觉

一个极简例子:

  • 分支 A 的值落在区间 $[3, 5]$;
  • 分支 B 的值落在区间 $[5, 100]$。

你(max 节点)会选哪个?无论 A、B 最终精确值是多少,B 一定不比 A 差——所以根本不用把两者都算精确。只要区间不重叠,就能提前下结论。这个思想叫分支限界(branch and bound),在组合优化里被大量使用。

再看一棵树:根 $= \max\big(3,\ \min(2, X)\big)$。左子已算出为 3;右子是个 min 节点,已经看到一个子节点是 2,所以右子的最终值 $\le 2$。既然右子最多 2、比 3 小,根一定取左边的 3——$X$ 无论是多少都不用探索了,剪掉!

graph TD
    R["root (max)<br/>= max(3, ≤2) = 3"]
    R -->|已算| L["左: min = 3"]
    R -->|"部分"| M["右 (min)<br/>≤ 2 (已见 2)"]
    M --> V2["叶 = 2"]
    M -.✂️ 剪枝.-> X["X (无需探索)"]

    style R fill:#ffe0b2,stroke:#e65100
    style L fill:#c8e6c9,stroke:#2e7d32
    style M fill:#e1f5fe,stroke:#0277bd
    style V2 fill:#e1f5fe,stroke:#0277bd
    style X fill:#ffcdd2,stroke:#c62828

4.2 一般机制:用 α / β 界剪枝

深度优先探索的过程中,沿路径维护两个界:

  • $\alpha$:max 节点的下界——沿当前路径,agent 已经能保证拿到的最好值(随着 max 节点探索更多子节点,$\alpha$ 只升不降,$\alpha \leftarrow \max(\alpha, v_{\text{child}})$);
  • $\beta$:min 节点的上界——沿当前路径,对手已经能把 agent 压到的最低值(随着 min 节点探索更多子节点,$\beta$ 只降不升,$\beta \leftarrow \min(\beta, v_{\text{child}})$)。

minimax 的最终价值来自某个叶子;从根到那个叶子、由双方最优策略走出的路径叫最优路径(optimal path)。最优路径上的值必须同时满足路径上所有祖先的界——落在 $[\alpha, \beta]$ 区间内。

说明

当某节点的界与它任一祖先的界不再重叠——即 $\alpha \ge \beta$、可行区间 $[\alpha,\beta]$ 变空——就剪掉该节点剩余的子树:最优路径不可能经过这里。初始时 $\alpha=-\infty,\ \beta=+\infty$(还什么都不知道)。

graph TD
    R["根 max<br/>α 累积上升"]
    R --> N1["min 节点<br/>β 累积下降"]
    N1 --> G1["已探索子树<br/>确定 min 的 β"]
    N1 --> P["下一个子节点"]
    P -.✂️ α ≥ β 则剪.-> SUB["整片子树跳过"]

    style R fill:#c8e6c9,stroke:#2e7d32
    style N1 fill:#e1f5fe,stroke:#0277bd
    style G1 fill:#e1f5fe,stroke:#0277bd
    style P fill:#ffe0b2,stroke:#e65100
    style SUB fill:#ffcdd2,stroke:#c62828

提示

可以把 $\alpha$ 读作”max 方已经握在手里的保底“,$\beta$ 读作”min 方已经能实施的封顶“。当保底 $\ge$ 封顶,双方在这条支线上已经”谈崩”——max 方在祖先处有更好的选择,min 方也不会允许比封顶更高的结果发生,这条支线注定与最终答案无关,继续算只是浪费时间。

4.3 走子顺序(move ordering)很关键

注意

子节点被访问的顺序不影响结果(剪枝是精确的),但直接决定你能剪掉多少。同一棵树,好的顺序能剪掉大半,坏的顺序几乎剪不动。

实践中用启发式(比如下一节的评估函数)给子节点排序,让界尽快收紧:

  • max 节点:按估值从大到小(decreasing)排——先试最可能好的动作,早早抬高 $\alpha$;
  • min 节点:按估值从小到大(increasing)排——先试对 agent 最狠的动作,早早压低 $\beta$。

最优排序下,α-β 能把 minimax 的时间复杂度从 $O(b^{d})$ 降到约 $O(b^{d/2})$——相当于每层有效分支从 $b$ 缩到 $\sqrt{b}$,也意味着同样的计算预算能把搜索深度翻倍,这在实战棋力上是巨大的差距;最坏排序则退化回纯 minimax,几乎剪不掉任何东西。


5. 评估函数:深度受限的近似

即便有 α-β 剪枝,国际象棋(分支因子 $b \approx 35$)、围棋($b \approx 250$)的完整树仍然根本算不完——指数就是指数。剪枝是精确加速,还需要一个近似手段。

说明

博弈里真正定义好坏的只有终局输赢,但等不到终局。评估函数用先验知识非终止状态上估一个”这个局面看起来有多好”的分数 $\mathrm{Eval}(s)$,用来顶替”把子树展开到底”。

深度受限的递归:搜到深度 $d$ 就停,用 $\mathrm{Eval}(s)$ 顶替真正的 $V(s)$:

$$V(s, d)=\begin{cases}\text{utility}(s), & \mathrm{isEnd}(s)\\[2pt] \mathrm{Eval}(s), & d = 0 \text{(深度耗尽)}\\[2pt] \max_{a} V\big(\mathrm{succ}(s,a),\, d-1\big), & \mathrm{player}(s)=\text{agent}\\[2pt] \min_{a} V\big(\mathrm{succ}(s,a),\, d-1\big), & \mathrm{player}(s)=\text{opp}\end{cases}$$

其中 $d$ 是剩余可搜索的层数,每递归一层减 1。

例子

给每种棋子赋权——后 9、车 5、象/马 3、兵 1——取双方之差:
$$\mathrm{Eval}(s) = 9\,(Q - Q') + 5\,(R - R') + 3\,(B - B' + N - N') + 1\,(P - P')$$
其中 $Q, R, B, N, P$ 是己方后/车/象/马/兵的数量,带撇的是对方的数量。真实引擎还会叠加位置(兵形、王的安全)、机动性等项,但骨架就是这样一个手工加权和。

注意

深度受限 + 评估函数没有任何最优性保证——评估函数只是猜测,搜索深度也有限,可能错过真正的最优着法(经典失败模式:地平线效应,把坏事推到搜索深度之外就”看不见”了)。它是”用先验知识换速度“的近似。

说明

类比:$\mathrm{Eval}(s)$ 与 Lecture 6 · UCS 与 A<em> 搜索(未找到对应页面) 里的 FutureCost / 启发式 $h(s)$* 一脉相承——都是”当前状态到终局还有多好/多远”的估计。区别在于:A* 的启发式若可采纳(admissible)就仍保证最优解,而博弈的评估函数一般没有这种保证。深度受限 minimax + 手工评估函数 + α-β 剪枝,正是深蓝(Deep Blue)等经典棋类引擎的骨架。

提示

这个 $\mathrm{Eval}(s)$ 从哪来?国际象棋可以靠几百年人类棋理手工设计,但这条路又贵又不通用。更强大的做法是从对弈数据里把评估函数学出来——把 $\mathrm{Eval}(s)$ 变成带参数的 $V(s; \mathbf{w})$,用强化学习拟合。这正是 Lecture 11 · 博弈 II:TD 学习与同时博弈(TD 学习、自我对弈)的主题。


6. 总结

mindmap
  root((博弈 I))
    形式化
      两人零和博弈
      Game 五元组接口
      效用只在终止状态·稀疏奖励
      不同状态由不同玩家控制
    策略
      确定性策略
      随机策略
    四种递归
      V_eval 双方策略给定求期望
      V_exptmax 对手已知取期望
      V_minmax 对手对抗取 min
      expectiminimax 加机会节点
    minimax 性质
      对最坏对手最优
      对任意对手是下界
      知道对手时 expectimax 更好
    加速
      α-β 剪枝·分支限界·精确
      走子顺序 max 降序 min 升序
      评估函数·深度受限·近似

关键要点

  • 博弈 = 对手未知的对抗:MDP 的环境随机且已知/可学,博弈里对手主动跟你作对且策略未知。
  • Game 五元组start_state / successors / player / is_end / utility;所有效用在终止状态(稀疏奖励);不同状态由不同玩家控制。
  • 一套递归框架、改汇聚算子:$V_{\text{eval}}$(双方按策略加权)→ $V_{\text{exptmax}}$(agent 取 $\max$、opp 取期望)→ $V_{\text{minmax}}$(agent 取 $\max$、opp 取 $\min$)→ expectiminimax(再加机会节点求期望)。
  • Minimax 原则:假设对手最小化你的效用;它对最坏对手最优,同时给出面对任意对手的价值下界;若真了解对手,expectimax 能做得更好——“最优”永远相对于对手假设而言。
  • α-β 剪枝:用分支限界跳过不可能被最优路径经过的子树,精确不改结果;剪枝条件 $\alpha \ge \beta$;走子顺序决定剪枝效率,最优排序下复杂度约 $O(b^{d/2})$,等价于搜索深度翻倍。
  • 评估函数:深度受限时用先验知识(如棋子物质分)近似 $V$,牺牲最优性换速度,类比 Lecture 6 · UCS 与 A* 搜索 的 FutureCost。

下一讲预告:评估函数不该靠手写——能不能从对弈中出 $V(s;\mathbf{w})$?→ TD 学习(类比 SARSA : $Q_\pi$ :: TD : $V_\pi$,TD-Gammon 与自我对弈);再放开”回合制”假设,进入同时博弈(Morra、纯/混合策略、minimax 定理与纳什均衡)→ Lecture 11 · 博弈 II:TD 学习与同时博弈


复习自测

题目

expectimax 假设对手各 0.5 随机:A 分支期望 $0$、B 分支 $2$、C 分支 $5$,故选 C。minimax 假设对手最狠:A 分支被压到 $-50$、B 分支 $1$、C 分支 $-5$,取 $\max(-50, 1, -5)$ 故选 B。原则:最优策略内建了对手假设——对手模型一变,最优动作就变;谈”最优”必须先答”相对于谁”。

题目

half($3 \to 1$)。对手面对 $n=1$,无论 decrement 还是 half 都把 $n$ 变成 0,而”面对 0 的人赢”,于是轮到你时 $n=0$,你赢。若选 decrement($3\to2$),对手可把 $n=1$ 留给你,你反过来必输。故 $V_{\text{minmax}}(3) = \max(-1, +1) = +1$。

题目

因为它只剪掉已被证明不可能被最优路径经过的子树:当某节点的可行区间 $[\alpha, \beta]$ 变空($\alpha \ge \beta$),意味着 max 方在祖先处已有 $\ge \alpha$ 的保底、而这条支线最多只能给到 $\beta \le \alpha$,其精确值无论是多少都不会改变任何祖先的取值。走子顺序只决定界收紧的快慢——先探到好动作就能早剪、多剪(最优排序约 $O(b^{d/2})$),但无论何种顺序,没被证明无关的子树都会被算到,结果恒等于纯 minimax。

题目

$V(\pi_{\text{exptmax}(7)}, \pi_{\min}) \le V(\pi_{\max}, \pi_{\min}) \le V(\pi_{\max}, \pi_{7}) \le V(\pi_{\text{exptmax}(7)}, \pi_{7})$。链条说明:minimax 价值是面对任意对手的保底;对手越弱 agent 拿得越多。但当你确切知道对手的(次优)策略 $\pi_7$ 时,针对它的 expectimax 应对严格不差于 minimax(右端不等式)——此时 minimax 因过度保守而不是最佳选择。反之若对手模型猜错而对手其实很强,expectimax 策略会跌破 minimax 保底(左端不等式)。

题目

同:都是对”从当前状态到终局还有多少价值/代价”的估计,都用先验知识替代不可承受的完整搜索。异:A* 中只要 $h$ 可采纳(不高估真实剩余代价),算法仍保证最优解;博弈中评估函数没有类似的”可采纳性”理论,深度受限 minimax 没有最优性保证,评估函数的质量直接决定棋力,这也是下一讲要”学”评估函数的动机。


参考资料