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 个时也能通过,因为棋盘很小。