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

> **CS221: Artificial Intelligence — Principles and Techniques** · Stanford · Autumn 2025
> 📅 Oct 22 · 💻 [`games.py`](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/games.py)

---

## 从 MDP 到博弈

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

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

**动机例子**：一棵小游戏树（下文 `Game1`），你在根节点先手，你该选哪个动作？

> [!tip] 直觉
> 你的最优策略**取决于对手怎么下**。如果对手随机乱走，和对手拼命想赢你，你算出来的最优动作完全不同（本讲会在同一棵树上算出两个不同答案：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 的角度），对手的目标自动就是最小化它。

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

### 1.1 Game 的接口

一个博弈由下面五个部分定义（对比搜索/MDP：多了一个 `player`，因为不同状态由不同玩家控制）：

```python
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 在决策，博弈里控制权在玩家之间交替。

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

### 1.2 例子 1：一棵小游戏树

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

```python
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 层、🟧 叶子效用）：

```mermaid
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$ 不足以判断局面。

```python
@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
```

> [!warning] 易错点
> 别把规则记反：不是"把对手逼到 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` 让轮到的玩家各按自己的策略采样动作，一路走到终局：

```python
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$ + 机会节点求期望 | 无对应 |

> [!tip] 直觉
> 这四种递归其实在回答同一个问题的四个版本："这个局面值多少分？"——答案取决于**你对每一层的控制者做什么假设**：自己人取 $\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 的节点，都按该玩家自己的策略概率加权**——因为两边策略都已知，没有任何"选择"要做，只是算一个期望。

```python
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
```

> [!note]
> 这与 [[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 节点取**期望**（对手照它已知的分布走，我们对其随机性求平均）。

```python
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}")
```

> [!example] Game1 上的 expectimax
> 若对手随机（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 假设对手策略已知，太乐观——万一对手不是随机的，而是个高手呢？

> [!note] Minimax 原则
> 假设对手会下**他所能下的最强的一手**——即对手在每个节点都**最小化**你的效用。在这个最坏假设下求自己的最优策略。

$$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$——**不需要事先给定任何策略**，双方都被假设为完美对抗，策略从递归本身"涌现"出来。

```python
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$ 后的值）：

```mermaid
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
```

> [!tip] 直觉
> 对比 §2.2：同一棵树，expectimax 选 C（赌对手随机，追求期望 5），minimax 选 **B**（防对手最优，锁定保底 1）。C 分支虽然期望高，但最坏情况是 $-5$；B 分支哪怕对手完美应对也能拿到 $+1$。**对对手的假设不同，最优动作就不同。** minimax 在 MDP 里没有对应物——因为 MDP 的"另一半"从不对抗你。

> [!example] 减半游戏 $n=3$ 的手算
> 轮到 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 价值：

```python
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 仍可能翻盘。

> [!note] 关于"解出游戏"（solved games）
> 双方都按 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$）——它不是谁的"策略"，而是物理规律，所以永远取期望。

> [!note]
> 一旦想通"每类玩家节点用什么算子汇聚子节点"，这个框架可以**任意扩展**：多于两个玩家、某玩家连走两步、由状态决定下一个行动者……只要把递归写成一般形式即可。但也有游戏树**覆盖不了**的情形：
> - **不完美信息（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 最优应对。

> [!warning] 易错点
> 每个"最优"策略都**内建了对另一方怎么下的假设**。谈最优，永远要追问一句：**相对于谁最优（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})$$

> [!tip] 直觉
> 以 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$ 为树深，即总步数），**一般是指数时间**。怎么加速？

> [!note] α-β 剪枝（alpha-beta pruning）
> 不去访问那些**已经能断定不可能影响最终答案**的状态。关键性质：它**不改变 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$ 无论是多少都不用探索了，剪掉！**

```mermaid
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]$ 区间内。

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

```mermaid
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
```

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

### 4.3 走子顺序（move ordering）很关键

> [!warning] 易错点
> 子节点被访问的**顺序**不影响结果（剪枝是精确的），但**直接决定你能剪掉多少**。同一棵树，好的顺序能剪掉大半，坏的顺序几乎剪不动。

实践中用**启发式（比如下一节的评估函数）给子节点排序**，让界尽快收紧：

- **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$）的完整树仍然**根本算不完**——指数就是指数。剪枝是精确加速，还需要一个**近似**手段。

> [!note] 评估函数（evaluation function）
> 博弈里真正定义好坏的只有终局输赢，但等不到终局。评估函数用**先验知识**在**非终止状态**上估一个"这个局面看起来有多好"的分数 $\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。

> [!example] 国际象棋的物质评估函数（material evaluation）
> 给每种棋子赋权——后 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$ 是己方后/车/象/马/兵的数量，带撇的是对方的数量。真实引擎还会叠加位置（兵形、王的安全）、机动性等项，但骨架就是这样一个手工加权和。

> [!warning] 易错点
> 深度受限 + 评估函数**没有任何最优性保证**——评估函数只是猜测，搜索深度也有限，可能错过真正的最优着法（经典失败模式：地平线效应，把坏事推到搜索深度之外就"看不见"了）。它是"**用先验知识换速度**"的近似。

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

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

---

## 6. 总结

```mermaid
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 学习与同时博弈]]。

---

## 复习自测

> [!question]- Q1：同一棵 `Game1` 树，expectimax 选 C、minimax 选 B。各自的计算过程是什么？这说明了什么原则？
> expectimax 假设对手各 0.5 随机：A 分支期望 $0$、B 分支 $2$、C 分支 $5$，故选 C。minimax 假设对手最狠：A 分支被压到 $-50$、B 分支 $1$、C 分支 $-5$，取 $\max(-50, 1, -5)$ 故选 B。原则：**最优策略内建了对手假设**——对手模型一变，最优动作就变；谈"最优"必须先答"相对于谁"。

> [!question]- Q2：减半游戏中轮到你、$n=3$，最优动作是什么？请手算验证。
> 选 **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$。

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

> [!question]- Q4：写出 minimax 三条性质连成的不等式链，并说明"什么时候不该用 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 保底（左端不等式）。

> [!question]- Q5：博弈的评估函数与 A\* 的启发式 $h(s)$ 有何异同？
> 同：都是对"从当前状态到终局还有多少价值/代价"的估计，都用先验知识替代不可承受的完整搜索。异：A\* 中只要 $h$ 可采纳（不高估真实剩余代价），算法仍**保证最优解**；博弈中评估函数没有类似的"可采纳性"理论，深度受限 minimax **没有最优性保证**，评估函数的质量直接决定棋力，这也是下一讲要"学"评估函数的动机。

---

## 参考资料

- 💻 [games.py](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/games.py) — 本讲源码（Game / Game1 / HalvingGame / V_eval / V_exptmax / V_minmax）
- 📖 [Russell & Norvig: *Artificial Intelligence: A Modern Approach*](https://aima.cs.berkeley.edu/) — 第 5 章：对抗搜索与博弈（minimax、α-β、评估函数）
- 📄 [Programming a Computer for Playing Chess (Shannon, 1950)](https://www.pi.infn.it/~carosi/chess/shannon.txt) — 极小化极大 + 评估函数的开山之作
- 📄 [Some Studies in Machine Learning Using Checkers (Samuel, 1959)](https://ieeexplore.ieee.org/document/5392560) — 自对弈学习评估函数（引出 L11）
- 🔗 [α-β 剪枝可视化 (Autumn 2023 module)](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=games%2Falpha-beta-pruning.js&mode=print6pp) — 交互式游戏树演示
- 🌐 [CS221 课程主页](https://stanford-cs221.github.io/) — 全部讲义与模块
