B4534 博弈搜索 Minimax

桌面游戏:选空格

数字顺序固定,X 取 max,Y 取 min,终局看分差。

一、先回答问题

结论

这题不是模拟谁“随便填”,而是一个完整信息博弈。剩余数字顺序固定,所以搜索层数就是剩余数字个数。

dfs(t):从第 t 个数字开始,返回双方最优时的 x-y。
课堂安排

先把“当前数字”放进一个空格,观察对手如何回应。再把所有选择连成小博弈树:X 选最大,Y 选最小。

数字固定 枚举空格 X 取 max Y 取 min

二、核心观察

不要枚举数字顺序

第 1 轮一定填剩余数字里最小的,第 2 轮填次小的。数字出现顺序没有决策。

只枚举位置

每一层把当前数字试放到所有空格中,然后递归看后面最优结果。

分差是唯一目标

X 想让 x-y 越大越好;Y 想让 x-y 越小越好。平局就是最终分差等于 0。

三、互动演示:从样例 2 看 minimax

当前数字1
当前角色X 取 max
预测分差0
样例 2 的空格是左上、第一行第二列、第三行第二列。先点一个空格,试试数字 1 放在哪里。
X 放 1 在左上
后续最好可 tie

这是最优第一步。

X 放 1 在上中
second

Y 能压低 x-y。

X 放 1 在下中
second

列乘积会被 Y 利用。

四、自己选答案

问题 1:搜索时要枚举什么?

问题 2:Dr. Y 的递归层应该怎么合并答案?

五、手推结果

递归位置要做的事返回值含义
t == nums.size()棋盘已满,计算 3 行乘积和 x、3 列乘积和 y。返回 x-y。
t 为偶数X 操作,把 nums[t] 试放每个空格。所有子状态中取最大值。
t 为奇数Y 操作,把 nums[t] 试放每个空格。所有子状态中取最小值。
记忆化 key记录 t 和 9 个格子的当前数字。同一状态不用重复搜索。

六、C++14 参考代码

本地高亮代码:minimax + 记忆化
#include <bits/stdc++.h>
using namespace std;

int a[9];
vector<int> nums;
unordered_map<string, int> memo;

int score() {
    int x = 0, y = 0;
    for (int i = 0; i < 3; i++) {
        x += a[i * 3] * a[i * 3 + 1] * a[i * 3 + 2];
        y += a[i] * a[i + 3] * a[i + 6];
    }
    return x - y;
}

string key(int t) {
    string s;
    s.reserve(10);
    s.push_back(char('0' + t));
    for (int i = 0; i < 9; i++) s.push_back(char('0' + a[i]));
    return s;
}

int dfs(int t) {
    if (t == (int)nums.size()) return score();
    string k = key(t);
    if (memo.count(k)) return memo[k];

    int cur = nums[t];
    if (t % 2 == 0) { // Dr. X: maximize x-y
        int res = -1e9;
        for (int i = 0; i < 9; i++) {
            if (a[i] == 0) {
                a[i] = cur;
                res = max(res, dfs(t + 1));
                a[i] = 0;
            }
        }
        return memo[k] = res;
    } else {          // Dr. Y: minimize x-y
        int res = 1e9;
        for (int i = 0; i < 9; i++) {
            if (a[i] == 0) {
                a[i] = cur;
                res = min(res, dfs(t + 1));
                a[i] = 0;
            }
        }
        return memo[k] = res;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) {
        bool used[10] = {};
        for (int i = 0; i < 9; i++) {
            cin >> a[i];
            if (a[i]) used[a[i]] = true;
        }
        nums.clear();
        for (int x = 1; x <= 9; x++) {
            if (!used[x]) nums.push_back(x);
        }
        memo.clear();
        int diff = dfs(0);
        if (diff > 0) cout << "first\n";
        else if (diff < 0) cout << "second\n";
        else cout << "tie\n";
    }
    return 0;
}
读代码 1

nums 只存还没出现的数字,天然从小到大。

读代码 2

t % 2 决定当前是 X 取最大,还是 Y 取最小。

复杂度

最多 9 个空格,搜索树不超过 9!,记忆化后重复状态会被缓存。

七、容易写错

误区

把 X 和 Y 分别算自己的最高分。真正目标不是各自独立最大,而是同一个分差 x-y 的 max/min。

边界

空格只有 1 个时也要走 dfs;空格为 9 个时也能通过,因为棋盘很小。