取球游戏
题目描述
今盒子里有 nn 个小球,A、B 两人轮流从盒中取球,每个人都可以看到另一个人取了多少个,也可以看到盒中还剩下多少个,并且两人都很聪明,不会做出错误的判断。
我们约定:
每个人从盒子中取出的球的数目必须是:1,3,7 或者 8 个。轮到某一方取球时不能弃权!A 先取球,然后双方交替取球,直到取完。被迫拿到最后一个球的一方为负方(输方)
请编程确定出在双方都不判断失误的情况下,对于特定的初始球数,A 是否能赢?
输入描述
先是一个整数 n (n<100)n (n<100),表示接下来有 nn 个整数。
然后是 nn 个整数,每个占一行(整数< 104104),表示初始球数。
输出描述
程序则输出 nn 行,表示 A 的输赢情况(输为 0,赢为 1)。
输入输出样例
示例
输入
4
1
2
10
18

输出
0
1
1
0

运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
总通过次数: 1127 | 总提交次数: 1221 | 通过率: 92.3%
难度: 困难 标签: 2012, 省赛, 博弈
算法思路:动态规划博弈分析
本题是经典的博弈论问题,核心在于分析不同球数下的必胜态(Winning State)和必败态(Losing State)。根据规则,每次可取球数限定为 {1, 3, 7, 8},被迫取最后一个球的一方输。我们采用动态规划(DP)求解:
-
状态定义:
dp[i]表示剩余i个球时,当前行动者(即将取球的人)是否能获胜dp[i] = 1:当前行动者可获胜dp[i] = 0:当前行动者必败
-
状态转移:
- 若存在一种取法(取
k ∈ {1,3,7,8}个球),使对方处于必败态(dp[i-k] = 0),则dp[i] = 1 - 若所有取法均使对方处于必胜态(
dp[i-k] = 1),则dp[i] = 0
- 若存在一种取法(取
-
边界条件:
dp[0] = 1(无球时,上一位玩家已取最后球,当前玩家获胜)dp[1] = 0(只能取1球,取后对方处于dp[0]=1的必胜态)
算法步骤图解
剩余球数 i | 可取球数 k | 剩余 i-k | dp[i-k] | 是否获胜
------------------------------------------
i=1 | k=1 | 0 | 1 | ❌ (dp[1]=0)
i=2 | k=1 | 1 | 0 | ✅ (dp[2]=1)
i=3 | k=1 | 2 | 1 | ❌
| k=3 | 0 | 1 | ❌ (dp[3]=0)
i=4 | k=1 | 3 | 0 | ✅ (dp[4]=1)
i=10 | k=3 | 7 | 0 | ✅ (dp[10]=1)
C++完整代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
// 读取输入数据
int n;
cin >> n;
vector<int> balls(n);
int max_val = 0;
for (int i = 0; i < n; i++) {
cin >> balls[i];
max_val = max(max_val, balls[i]);
}
// 初始化DP数组(大小设为10005防止越界)
vector<int> dp(10005, 0);
dp[0] = 1; // 边界条件:无球时当前玩家获胜
// 动态规划计算所有状态(1到max_val)
for (int i = 1; i <= max_val; i++) {
dp[i] = 0; // 默认设为必败态
// 尝试四种取球策略
int moves[] = {1, 3, 7, 8};
for (int k : moves) {
if (i >= k && dp[i - k] == 0) {
dp[i] = 1; // 存在使对手必败的策略
break;
}
}
}
// 输出结果
for (int x : balls) {
cout << dp[x] << endl;
}
return 0;
}
代码解析
-
输入处理:
- 读取测试数据量
n - 存储所有初始球数并计算最大值
max_val(优化DP计算范围)
- 读取测试数据量
-
DP数组初始化:
- 创建
dp[10005]确保覆盖最大球数(10^4) - 设置
dp[0]=1关键边界条件8
- 创建
-
状态转移:
- 遍历球数
i从 1 到max_val - 对每个
i尝试四种取球策略k ∈ {1,3,7,8} - 若
i>=k且dp[i-k]==0(对手必败),则dp[i]=1
- 遍历球数
-
结果输出:
- 直接查询
dp[x]输出每个初始球数的胜负
- 直接查询
实例验证(样例输入:1,2,10,18)
| 初始球数 | 计算过程 | 输出 | 是否符合样例 |
|---|---|---|---|
| 1 | dp[1] = 0(唯一取法使对方必胜) | 0 | ✅ |
| 2 | dp[2-1]=dp[1]=0 → 获胜 | 1 | ✅ |
| 10 | dp[10-3]=dp[7]=0 → 获胜 | 1 | ✅ |
| 18 | 所有取法均使对手必胜 | 0 | ✅ |
关键测试点
-
边界值测试:
- 球数=0(非法输入,但程序返回
dp[0]=1) - 球数=1(必败态,输出0)
- 球数=7(必败态,
dp[7]=0) - 球数=8(必胜态,
dp[8]=1)
- 球数=0(非法输入,但程序返回
-
特殊序列验证:
球数: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 DP: 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1 1 0 1 0 -
最大性能测试:
- 输入100个球数(均接近10^4),DP计算仅需40000次迭代(<0.1s)
优化建议
-
预处理优化:
// 预先计算全局DP表(避免重复计算) vector<int> dp(10005, 0); dp[0] = 1; for (int i = 1; i <= 10000; i++) { for (int k : {1, 3, 7, 8}) { if (i >= k && !dp[i - k]) { dp[i] = 1; break; } } } // 后续直接查询dp表- 优势:多组查询时时间复杂度降为 O(1)
-
数学优化(周期性):
- 观察DP序列:
[1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,0,1,0]... - 发现 每16球一个循环周期(1-16, 17-32...状态相同)
- 优化代码:
int result[16] = {1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,0}; for (int x : balls) { cout << result[(x - 1) % 16] << endl; } - 优势:无需DP计算,直接数学输出
- 观察DP序列:
注意事项
-
状态定义一致性:
dp[i]始终表示 当前行动者 的胜负状态- 初始时A先手,故直接查询
dp[x]即为结果
-
数组越界防护:
- DP数组大小设为
10005(>10^4)避免越界 - 取球前检查
i >= k
- DP数组大小设为
-
最优策略性质:
- 双方均采用最优策略时,先手胜负仅由球数决定
- 无需模拟具体取球过程

2433

被折叠的 条评论
为什么被折叠?



