博弈论1
第一题:取沙子游戏
- 题目简述:有 n 粒沙子,Alice 先手,轮流取。
- 规则:第一次取:取 1 ~ min(n, k) 任意粒。之后每次取的数量必须是之前所有已取数量的最大公约数 的约数,且不能超过k,也不能超过剩余沙子数。取完最后一粒的人获胜。
- 问:在双方都绝顶聪明的情况下,Alice 能否获胜?
题目链接:https://qoj.ac/problem/14532
先定义当前状态为 (s, g),其中 s 表示剩余量,g 表示当前的限制,即前面所有数的 gcd。
- 当
s为奇数时,直接取1,就能转化成(偶数,1)的情况,是必败态,所以s为奇数时一定是必胜态。 - 接下来只需要分析
s为偶数。
当g为奇数时,肯定只存在奇数因子,状态就会转变为(s-a, a),因此s为偶数,g为奇数时败。
设 s = p * 2^m,g = q * 2^n,下一步取的数量为 t * 2^u。
情况一:u < m
下一个状态转变为 (2^u * (p * 2^(m-u) - t), t * 2^u),此时 s、g 的 2 的幂次相同,可以得到这个状态为必胜态。证明如下:
- 只要取
x = k * 2^u就相当于(p * 2^(m-u) - t - k, k)的变化,属于必败态。(s 中 2 的因子更多) - 下一步如果想要打破状态,就只能取
c * 2^v,v < u。状态转变为(2^v * (even - odd), c * 2^v),回到了之前的必胜态。
综上所述,当取u < m时,必败。
情况二:u = m
下一个状态为 (2^u * (p - t), t * 2^u),形同上述的 1.1。因此 u = m 必胜。
情况三:u > m
状态变为 (2^m(p - t * 2^(u-m)), t * 2^u),此时 s 的 2 因子少于 g,只需选择 v < m 的情况,就能把 s 中 2 的因子变得比 g 中多,变成必败态。
因此只有取 u = m 才能维持必胜态,第一步的 g 就要取到 lowbit(n),因此只要比较 lowbit(n) 和 k 就可以解决。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t; cin >> t;
while(t --) {
ll n, k;
cin >> n >> k;
ll lowbit = n & -n;
if(lowbit <= k) cout << "Alice\n";
else cout << "Bob\n";
}
return 0;
}
第二题:数字消失术
-
题目大意: Alice 和 Bob 轮流从一个正整数中删除一个数字。每次删除后,产生的所有前导零会被一并删除(例如从 105 中删除百位的 1 得到 5,删除十位的 0 得到 15,删除个位的 5 得到 10)。
删除最后一个非零数字的玩家获胜。
定义 ( W(N) ) 为所有小于 ( N ) 的正整数中,在双方都采取最优策略下先手玩家必胜的数的个数。
给定 ( N ),请你计算 ( W(N) )。 -
输入描述
一行一个整数 ( N )(( 1 \le N \le 10^{18} ))。 -
输出描述
一行一个整数,表示 ( W(N) )。 -
数据范围
( 1 \le N \le 10^{18} )
题目链接:https://ac.nowcoder.com/acm/contest/136171/F先考虑没有0的情况,显然长度为奇数的为必胜态。
接下来考虑有0存在的情况,就相当于在d * 10^n + r 这里的状态就取决于后续r的状态。如果r是奇数,就是必胜,此时如果第一步去掉d,就变成了,胜态,所以先手不会这么做,这就取决于中间0的数量,如果0的数量是奇数,就是必胜态,偶数则是必败态。而r为偶数则相反。
由此可见,只要n的长度为奇数,则总是必胜态。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll dp[20][2], sum[20][2], pw[20];//pw 10的幂次,dp 位置i的输赢数量,sum 到i的输赢数量
//两个函数的写法 可以参考
// ll win(ll n);
// ll lose(ll n) {//先手输的数量
// if(n <= 1) return 0;
// string s = to_string(n);
// int len = s.size();
// ll ans = sum[len-1][0];
// if(len % 2 == 0) {
// int first = n / pw[len-1];
// ans += (first-1) * sum[len-1][1];
// ll r = n % pw[len-1];
// ans += win(r);
// }
// return ans;
// }
// ll win(ll n) {//先手赢的数量
// if(n <= 1) return 0;
// return n - 1 - lose(n);
// }
一个函数的写法
ll cal(ll n) {
if(n <= 1) return 0;
string s = to_string(n);
int len = s.size();
ll ans = sum[len - 1][1];
int first = s[0] - '0';
ll r = n % pw[len - 1];
if(len % 2 == 1) {
ans += (first - 1) * pw[len - 1];
ans += r;
}
else {
ll loseCnt = sum[len - 1][0];
loseCnt += (first - 1) * sum[len - 1][1];
loseCnt += cal(r);
ans = (n - 1) - loseCnt;
}
return ans;
}
void solve() {
pw[0] = 1;
for(int i = 1; i < 20; i ++) pw[i] = pw[i-1] * 10;
for(int i = 1; i < 20; i ++) {
ll tol = pw[i - 1] * 9;
if(i & 1) dp[i][1] = tol, dp[i][0] = 0;
else dp[i][0] = sum[i-1][1] * 9, dp[i][1] = tol - dp[i][0];
sum[i][0] = sum[i-1][0] + dp[i][0];
sum[i][1] = sum[i-1][1] + dp[i][1];
}
ll n; cin >> n;
cout << cal(n) << endl;
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
solve();
return 0;
}
第三题:粑粑博弈
- 题目大意: 给定一个长度为
n的非负整数数组a和一个正整数k。Alice 和 Bob 轮流进行游戏,Alice 先手。
每轮操作,当前玩家需要选择:
一个正整数x(1 ≤ x ≤ k)
一个下标y(1 ≤ y ≤ n)
并且必须满足x ≤ a_y。
然后执行更新:
k变为x
a_y变为a_y - x
注意:如果玩家选择
x = 0或无法做出有效操作,则立即判负。
问:在双方都采取最优策略的情况下,先手 Alice 是否有必胜策略?
- 数据范围
测试用例组数:1 ≤ t ≤ 10^5
单组数组长度:1 ≤ n ≤ 10^6
所有测试用例的n之和:∑ n ≤ 2 × 10^6
初始上限:1 ≤ k ≤ 10^9
数组元素:0 ≤ a_i ≤ 10^9
题目链接:https://qoj.ac/contest/3758/problem/18279
虽然游戏中的上限 k 会不断变化,但通过定义一个二进制进位边界 p(严格大于当前 k 的最小 2 的幂),可以将整个游戏压缩成经典的 Nim 游戏。胜负完全由所有 a_i 对 p 取模后的异或和决定。
对于任意正整数 k,定义函数:
$$
P(k) = 2^{\lfloor \log_2 k \rfloor + 1}
$$
即 严格大于 k 的最小的 2 的幂。
例如:P(5)=8, P(6)=8, P(8)=16。
该函数具有两个关键性质(在证明中会反复使用):
- 对于任意合法操作
x(满足1 ≤ x ≤ k),必有x < P(k)。 - 若
p = P(k),则p/2 ≤ k < p。 - 任何新的上限
x所对应的新模数p' = P(x)一定是旧模数p的约数(因为p和p'都是 2 的幂,且p' ≤ p)。
- 单堆情形的严格证明
状态:(k, a),当前上限为 k,堆大小为 a。
结论:单堆状态下,必败态 当且仅当:
$$
a \bmod P(k) = 0
$$
- 证明“余数为 0 是必败态”(无法走向余数为 0)
设当前模数 p = P(k),且 a % p = 0。
当前玩家取 x(1 ≤ x ≤ k),新状态为 (x, a-x),新模数为 p' = P(x)。
- 因为
x < p且p是 2 的幂,所以p'整除p。 - 由于
a是p的倍数,而p' | p,因此a也是p'的倍数,即a % p' = 0。 - 计算新堆的余数:
$$
(a - x) \bmod p' = (0 - x) \bmod p' = (-x) \bmod p'
$$ - 由于
1 ≤ x < p'(因为p'是大于x的最小 2 的幂),所以x不可能是p'的倍数。 - 因此
(-x) mod p' ≠ 0,即新状态的余数不为 0。
结论:从余数为 0 的状态出发,无论如何走,都会走到余数非 0 的状态(即必胜态)。符合“必败态”定义。
- 证明“余数非 0 是必胜态”(总能走向余数为 0)
设当前模数 p = P(k),余数 r = a % p,显然 1 ≤ r < p。由于 p/2 ≤ k < p,分两种情况构造取法 x:
情况 A:若 r ≤ k
直接取 x = r。
- 取走后,
a - x是p的倍数。 - 新模数
p' = P(x)整除p,所以a - x也是p'的倍数。 - 新状态余数为 0,成功。
情况 B:若 r > k
令 q = p / 2,因为 r > k ≥ q,且 r < p = 2q,我们构造取 x = r - q。
- 合法性验证:因为
r > q,所以x ≥ 1;因为r < 2q,所以x < q ≤ k,故1 ≤ x ≤ k,合法。 - 计算新堆大小:
$$
a - x = a - (r - q) = (p \cdot t + r) - r + q = p \cdot t + q
$$
因为p = 2q,所以:
$$
a - x = 2q \cdot t + q = q \cdot (2t + 1)
$$
即a - x是q的倍数。 - 新模数
p' = P(x),因为0 < x < q,所以p'整除q。 - 因此
a - x也是p'的倍数,新状态余数为 0,成功。
结论:任意余数非 0 的状态,都能一步走到余数为 0 的状态。符合“必胜态”定义。
三、多堆情形的严格证明
状态:(k, a_1, ..., a_n),当前上限为 k。设 p = P(k)。
定义当前局面的 特征异或值 X:
$$
X = (a_1 \bmod p) \oplus (a_2 \bmod p) \oplus \dots \oplus (a_n \bmod p)
$$
其中 ⊕ 是按位异或运算。
结论:多堆状态下,必败态 当且仅当:
$$
X = 0
$$
- 证明“X=0 是必败态”(无法走向 X=0)
设当前模数 p = P(k),且 X = 0。
当前玩家选择 x(1 ≤ x ≤ k)修改第 y 堆:a_y → a_y - x。新状态模数为 p' = P(x),显然 p' | p。
对于除了第 y 堆以外的所有堆,其模 p' 下的余数不变。
因为原 X=0,在模 p 下异或为 0。由于 p' 是 p 的因子(相当于只取二进制低位),我们可以推导出新异或值:
$$
X' = \big( (a_y \bmod p) \bmod p' \big) \oplus \big( (a_y - x) \bmod p' \big)
$$
如果 X' = 0,则必须有:
$$
(a_y \bmod p') = (a_y - x) \bmod p'
$$
这等价于要求 x ≡ 0 (mod p')。
但是,因为 1 ≤ x < p'(p' 是严格大于 x 的 2 的幂),x 不可能整除 p',矛盾。
结论:从 X=0 出发,所有走法都会导致 X' ≠ 0。符合“必败态”定义。
- 证明“X≠0 是必胜态”(总能走向 X=0)
设当前模数 p = P(k),且 X ≠ 0。利用经典 Nim 博弈的思路构造操作。
在模 p 下,找到异或和最高位为 1 的那一堆(设为第 t 堆)。设该堆当前余数为 b = a_t mod p。令 b' = b ⊕ X,根据异或性质必有 b' < b。令标准 Nim 需要取走的数量为 r = b - b'。
由于 p/2 ≤ k < p,分两种情况构造:
情况 A:若 r ≤ k
直接取 x = r。
- 在模
p下,第t堆余数变为b',所有堆的异或和变为 0。 - 新模数
p' = P(x)整除p。既然在模p下异或为 0,那么在模p'(去掉高位)下异或也一定为 0。 - 新状态
X' = 0,成功。
情况 B:若 r > k
令 q = p / 2,构造取 x = r - q。
- 合法性:因为
r > k ≥ q,且r < p = 2q,所以1 ≤ r - q < q ≤ k,即x合法。 - 取走后,第
t堆在模p下的新余数为:
$$
b - x = b - (r - q) = b - r + q = b' + q
$$ - 新模数
p' = P(x),因为0 < x < q,所以p'整除q。因此(b' + q) mod p' = b' mod p'。 - 因为原构造保证了在模
p下所有堆异或为 0,取模p'后(去除高位)依然为 0,所以新状态X' = 0,成功。
结论:任意 X≠0 的状态,都能一步走到 X=0 的状态。符合“必胜态”定义。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t; cin >> t;
while(t --) {
int n;
long long k; cin >> n >> k;
long long p = 1;
while(p <= k) p <<= 1;
int xr = 0;
for(int i = 0; i < n; i ++) {
long long a; cin >> a;
xr ^= (int)(a % p);
}
cout << (xr == 0 ? "Bob" : "Alice") << '\n';
}
return 0;
}
第四题:巧克力游戏
-
题目大意: 有一块 n 行 m 列的巧克力,每格有一个正整数美味值 a_{i,j}。Putata 和 Budada 轮流吃,Putata 先手。每次操作可以选择吃掉剩余巧克力的最后 x 行(1 ≤ x ≤ 当前行数)或最后 y 列(1 ≤ y ≤ 当前列数),即只能从右下角切除一个连续的行块或列块。若某人操作后,剩余巧克力的总美味值 ≤ s,则此人被批评(输)。二人均采取最优策略避免被批评。
-
给定 q 个容忍度 s_i,对每个 s_i 判断最终谁会被批评。
-
输入格式
第一行一个整数 T,表示测试用例数。
每个测试用例:
第一行三个整数 n, m, q。
接下来 n 行,每行 m 个整数,表示 a_{i,j}。
接下来 q 行,每行一个整数 s_i。
对于每个查询,输出一行 "Putata"(如果 Putata 会被批评)或 "Budada"(如果 Budada 会被批评)。 -
数据范围
1 ≤ T ≤ 10^5
1 ≤ n·m ≤ 2×10^5
1 ≤ q ≤ 2×10^5
1 ≤ a_{i,j} ≤ 10^9
0 ≤ s_i < 所有 a_{i,j} 的总和
所有测试用例的 n·m 之和 ≤ 2×10^5 q 之和 ≤ 2×10^5
题目链接:https://qoj.ac/contest/2021/problem/10732
定义坏格为(i, j)为顶点和(1, 1)围成的矩形中甜度<=s的格子,用L表示。P表示必败态,N表示必胜态。列数大于行数,否则就转置。
当一个格子上 左都是L的时候,这个格子为必败态,或者往上方和左方延伸,只能传递到必胜态的也是必败态。而某一个格子为必胜态,只有从该格子方和左方延伸,找得到必败态的时候,才能定义为必胜态。而由此可以得出,L往下方和右方延伸的格子都必然是必胜态。
而存在特殊情况,有些格子无法被覆盖:
L L L L L L L L L L L P
L L P N N N L L P N N N
L P N N N N L P N N N N
L N N P N N L N N P N N
L N N N P N L N N N P N
L N N N N P L N N N N N
L L P N N
L P N N N
L N N P N
L N N N P
L N N N N
右下方3*2的格子是由其他已知的部分推算的。(3, 4)因为无法转移到P,所以是P,然后该点下方和右方都转变为N,以此类推
填充必然使用如下矩阵,从左上角裁剪:
P N N N N
N P N N N
N N P N N
N N N P N
N N N N P
而观察可以发现,这些填充格子必然会补全原本没有P的行列,因为复杂区域就是由没有P的行列产生,而由P的分布可知,第一次出现的特殊区域必定在现在推导的矩形的右下角。所以,只有当列数大于行数的时候,才能保证P不落在最后一格上面。当前面都满足,而最后一行满足为正方形时,P只能落在右下角。
定义f = i - e - (m - pi) 非满行-可用列。i为当前行,e是全为L的行数,pi是当前行的最大坏格列
必败条件:
前n行, f < 0 否则说明这一行的末尾存在P
第n行, f = 0
如果不存在复杂情况,那么(n-1, m-1)的区域必然全覆盖,而如果(1, m-1)为L,(1, m)不为,那么(1, m)为P。此时f=0
(n-1, 1)为L,而(n, 1)不为L,此时,非满行 = 可用列,说明下方是个正方形,而p不可能相邻,这就说明每个缝隙都被一个P填充
代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
int n, m, q; cin >> n >> m >> q;
vector<vector<ll>> a(n, vector<ll>(m));
for(int i = 0; i < n; i ++)
for(int j = 0; j < m; j ++) cin >> a[i][j];
//转置
if(n > m) {
vector<vector<ll>> b(m, vector<ll>(n));
for(int i = 0; i < n; i ++)
for(int j = 0; j < m; j ++) b[j][i] = a[i][j];
swap(n, m);
a = move(b);
}
vector<vector<ll>> pre(n + 1, vector<ll>(m + 1, 0));
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++) pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + a[i-1][j-1];
vector<pair<ll, int>> queries(q);// 离线查询
for(int i = 0; i < q; i ++) {
cin >> queries[i].first;
queries[i].second = i;
}
sort(queries.begin(), queries.end());
vector<int> p(n + 1, 0);
vector<int> ans(q);
for(auto &[s, idx] : queries) {
for(int i = 1; i <= n; i ++) {
while(p[i] < m && pre[i][p[i] + 1] <= s) p[i] ++;
}
int e = 0;
while (e < n && p[e + 1] == m) e ++;
bool ok = 1;
for(int i = e + 1; i < n; i ++) {
if(i - e - (m - p[i]) >= 0) {
ok = 0;
break;
}
}
if(ok && n > e && n - e - (m - p[n]) != 0) ok = 0;
ans[idx] = ok;
}
for(const int& ok : ans) cout << (ok ? "Putata" : "Budada") << '\n';
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
int t; cin >> t;
while(t --) solve();
return 0;
}

浙公网安备 33010602011771号