取球博弈
题目描述
两个人玩取球的游戏。
一共有 NN 个球,每人轮流取球,每次可取集合 n1,n2,n3n1,n2,n3中的任何一个数目。
如果无法继续取球,则游戏结束。
此时,持有奇数个球的一方获胜。
如果两人都是奇数,则为平局。
假设双方都采用最聪明的取法,
第一个取球的人一定能赢吗?
试编程解决这个问题。
输入描述
输入格式:
第一行 3 个正整数 n1,n2,n3 (0<n1,n2,n3<100)n1,n2,n3 (0<n1,n2,n3<100),空格分开,表示每次可取的数目。
第二行 5 个正整数 x1,x2,⋯,x5 (0<xi<1000)x1,x2,⋯,x5 (0<xi<1000),空格分开,表示 5 局的初始球数。
输出描述
输出一行 5 个字符,空格分开。分别表示每局先取球的人能否获胜。
能获胜则输出 +,次之,如有办法逼平对手,输出 0,无论如何都会输,则输出 -。
输入输出样例
示例
输入
1 2 3
1 2 3 4 5
输出
+ 0 + 0 -
运行限制
- 最大运行时间:3s
- 最大运行内存: 256M
总通过次数: 1061 | 总提交次数: 1355 | 通过率: 78.3%
难度: 困难 标签: 2016, 省赛, 动态规划, 博弈
算法思路:动态规划与状态压缩
本问题是一个博弈论问题,需要模拟双方轮流取球的过程,并根据最终球数的奇偶性判断胜负。核心思路是状态压缩+动态规划 ,将问题分解为四个维度的状态:剩余球数、先手奇偶性、后手奇偶性、当前玩家。通过自底向上的DP计算所有可能状态的最优策略结果。
算法步骤
-
状态定义:
dp[s][a][b][turn]
:剩余球数s
,先手奇偶性a
(0偶1奇),后手奇偶性b
,当前玩家turn
(0先手/1后手)- 值:
1
(先手赢)/0
(平)/-1
(先手输)
-
状态转移:
- 先手操作 (turn=0):尝试所有合法取球数
k
- 新先手奇偶性:
new_a = a ^ (k & 1)
- 转移到:
dp[s-k][new_a][b][1]
- 优先选赢(1),次选平(0),最后输(-1)
- 新先手奇偶性:
- 后手操作 (turn=1):尝试所有合法取球数
k
- 新后手奇偶性:
new_b = b ^ (k & 1)
- 转移到:
dp[s-k][a][new_b][0]
- 优先选先手输(-1),次选平(0),最后赢(1)
- 新后手奇偶性:
- 先手操作 (turn=0):尝试所有合法取球数
-
边界处理:
- 当
s=0
:根据奇偶性直接返回结果 - 当
s<min_take
:无法取球,直接结算
- 当
-
结果输出:
- 对每局初始球数
x
,查询dp[x][0][0][0]
1→'+'
,0→'0'
,-1→'-'
- 对每局初始球数
代码实现(C++)
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
// 读取取球规则
vector<int> takes(3);
cin >> takes[0] >> takes[1] >> takes[2];
int min_take = *min_element(takes.begin(), takes.end());
// 初始化DP数组 [s][a][b][turn]
int dp[1001][2][2][2];
const int UNCALC = INT_MIN;
// 初始化s=0的边界状态
for (int a = 0; a < 2; a++) {
for (int b = 0; b < 2; b++) {
for (int turn = 0; turn < 2; turn++) {
if (a == 1 && b == 0) dp[0][a][b][turn] = 1;
else if (a == 0 && b == 1) dp[0][a][b][turn] = -1;
else dp[0][a][b][turn] = 0;
}
}
}
// DP计算:s从1到1000
for (int s = 1; s <= 1000; s++) {
for (int a = 0; a < 2; a++) {
for (int b = 0; b < 2; b++) {
for (int turn = 0; turn < 2; turn++) {
// 默认无法取球时直接结算
if (s < min_take) {
if (a == 1 && b == 0) dp[s][a][b][turn] = 1;
else if (a == 0 && b == 1) dp[s][a][b][turn] = -1;
else dp[s][a][b][turn] = 0;
continue;
}
if (turn == 0) { // 先手操作
int best = UNCALC;
for (int k : takes) {
if (k > s) continue;
int new_a = a ^ (k & 1); // 更新奇偶性
int res = dp[s - k][new_a][b][1]; // 转后手
if (res == 1) { // 找到必胜策略
best = 1;
break;
} else if (res == 0) {
best = 0; // 标记平局可能
} else if (best == UNCALC) {
best = -1; // 无更优选择
}
}
dp[s][a][b][turn] = (best == UNCALC) ?
((a == 1 && b == 0) ? 1 : (a == 0 && b == 1) ? -1 : 0) : best;
} else { // 后手操作
int best = UNCALC;
for (int k : takes) {
if (k > s) continue;
int new_b = b ^ (k & 1); // 更新奇偶性
int res = dp[s - k][a][new_b][0]; // 转先手
if (res == -1) { // 找到必杀策略
best = -1;
break;
} else if (res == 0) {
best = 0; // 标记平局可能
} else if (best == UNCALC) {
best = 1; // 无更优选择
}
}
dp[s][a][b][turn] = (best == UNCALC) ?
((a == 1 && b == 0) ? 1 : (a == 0 && b == 1) ? -1 : 0) : best;
}
}
}
}
}
// 处理5局游戏
vector<int> x(5);
for (int i = 0; i < 5; i++) cin >> x[i];
for (int i = 0; i < 5; i++) {
int res = dp[x[i]][0][0][0];
if (res == 1) cout << '+';
else if (res == 0) cout << '0';
else cout << '-';
if (i < 4) cout << ' ';
}
return 0;
}
代码解析
-
状态压缩:
- 使用
a
/b
的0/1表示奇偶性,将球数状态压缩为4个维度 dp[s][a][b][turn]
存储当前状态的最优结果
- 使用
-
核心转移逻辑:
- 先手策略(L42-55):优先选择使后手必输的操作(返回1),次选平局
- 后手策略(L56-69):优先选择使先手必输的操作(返回-1),次选平局
- 奇偶更新 :
new_a = a ^ (k & 1)
(异或等效模2加法)
-
边界处理:
s=0
时直接根据奇偶性判定(L24-28)s<min_take
时无法操作,直接结算(L35-39)
-
结果查询:
- 初始状态:
dp[x][0][0][0]
(双方0球,先手操作) - 结果映射:
1→+
/0→0
/-1→-
- 初始状态:
实例验证
输入:1 2 3
+ 1 2 3 4 5
输出 :+ 0 + 0 -
状态分析:
- 球数1 :先手取1→剩0球,先手奇数(1)后手偶数(0)→先手赢
+
- 球数2 :
- 取1:转状态(1,1,0,1)→后手取1→(0,1,1,0)→平局
- 取2:转状态(0,0,0,1)→平局
- 最优平局
0
- 球数3 :取3→(0,1,0,1)→先手赢
+
- 球数4 :
- 取1:转(3,1,0,1)→后手取3→(0,1,1,0)→平局
- 取2/3:后手可迫使平局
- 最优平局
0
- 球数5 :
- 所有走法后手可迫使先手输
-
- 所有走法后手可迫使先手输
输入:1 4 5
+ 10 11 12 13 15
输出 :0 - 0 + +
(符合样例)
注意事项
-
奇偶性更新:
- 使用异或运算
a^(k&1)
等效模2加法 - 例:奇(1)+奇(1)→偶(0):
1^1=0
- 使用异或运算
-
不可操作状态:
- 当
s<min_take
时直接结算 - 需单独处理避免越界
- 当
-
DP初始化:
s=0
时turn维度无意义,但需完整初始化- 使用
UNCALC
标记未计算状态
-
策略优先级:
- 先手:赢→平→输(找到赢立即break)
- 后手:输→平→赢(找到输立即break)
多方位测试点
测试类型 | 测试数据 | 预期结果 | 验证要点 |
---|---|---|---|
最小球数 | 1 2 3 + 1 |
+ |
边界处理 |
不可操作 | 2 3 4 + 1 |
0 |
直接奇偶结算 |
全奇取胜 | 1 3 5 + 3 |
+ |
奇偶组合逻辑 |
强制平局 | 2 2 2 + 4 |
0 |
无最优解的平局处理 |
后手必杀 | 1 4 5 + 11 |
- |
后手策略优先级 |
大球数性能 | 1 2 3 + 1000 |
<1s |
DP效率验证 |
重复取法 | 3 3 3 + 6 |
0 |
操作去重逻辑 |
优化建议
-
循环优化:
cpp// 预处理合法操作(去重排序) sort(takes.begin(), takes.end()); takes.erase(unique(takes.begin(), takes.end()), takes.end());
-
维度压缩:
- 奇偶状态仅需2位,可用位运算压缩为单整数
cppint state = (a << 2) | (b << 1) | turn;
-
记忆化搜索:
cpp// 替代迭代DP,减少无效计算 int dfs(int s, int a, int b, int turn) { if (dp[s][a][b][turn] != UNCALC) return dp[s][a][b][turn]; // ...递归计算 }
-
剪枝策略:
- 当找到必胜/必杀策略时立即跳出循环
- 倒序排序取法,优先尝试大数加速命中