CF2032F - Peanuts
分析
本题如果只有一个盒子,因为最后一个操作的人是赢家,所以就是 Nim 游戏,先手必胜的充要条件是所有 \(a_i\) 异或和不为 \(0\)。
再考虑有两个盒子,显然最后一段必须是 Nim 游戏,我们试着从最后一段的状态反推出前一段的情况。
- 如果后一段满足先手必败,Alice 想要赢就必须让 Jack 成为后一段的先手。也就是说前一段需满足先手必胜。
- 如果后一段满足先手必胜,Alice 想要赢就必须让 Jack 拿走前一段的最后一堆,这样她就可以趁机成为第二段的先手。即前一段需满足先手必败。
说明一下,上述所说的所有先手必胜或必败都是针对一个盒子内的先手。之所以这么想,就是把盒与盒之间独立出来,这样才可能挖掘出 dp 需要的最优子结构。
说回来,上述第二种情况中,Alice 和 Jack 都想让自己在第一局输掉,这是 反常游戏(取走最后一颗棋子的人是败者),先手必胜的条件有所不同:
-
若 \(a_i\) 全为 \(1\),先手必胜当且仅当个数为偶数。
-
否则,需满足 \(a_i\) 异或和不为 \(0\)。
否则就是先手必败。
掌握了两个盒子的情况怎么合并,我们就可以开始设计 dp 状态了。
状态设计
记 \(f_{i, 0 / 1}\) 表示前 \(i\) 个数,最后一段是 Nim 还是反常游戏,整局游戏先手必胜的方案数。
注意:不同游戏结尾,其必胜含义不同。
答案:\(f_{n, 0}\),理由同上,最后一段必须是 Nim 游戏。
\(f_{i, 0}\) 的转移:最后一段先手必胜,从 \(f_{i, 1}\) 转移;最后一段必败,从 \(f_{i, 0}\) 转移。
\(f_{i, 1}\) 的转移:最后一段先手必胜,从 \(f_{i, 0}\) 转移;最后一段必败,从 \(f_{i, 1}\) 转移。
直接做是 \(O(n^2)\) 的,考虑如何优化。
总的来说,胜负有两个因素决定:异或和是否为 \(0\)、全 \(1\) 情况下的奇偶性。
- 对于异或和,判断一段异或和是否为 \(0\),可以处理异或前缀和 \(s_i\),则转化成 \(s_i\) 是否和 \(s_j\) 相等。因此可以用桶存下每种 \(s_i\) 能转移的贡献,类似的桶优化 dp 技巧也出现在了CSP-S 2024 染色,具体实现可参考我的这篇博客。
- 对于全 \(1\) 的奇偶性,简单统计就可以了。遇到全 \(1\) 的时候,第一种情况就暂时不更新,等全 \(1\) 中断后再把这一段更新进桶里面,有点类似双指针。
值域过大,用 map 可以做到 \(O(nlogn)\),用哈希表或许可以 \(O(n)\) 解决。
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
#define int ll
using namespace std;
using ll = long long;
const int N = 1e6 + 105;
const int mod = 998244353;
int T, n, sum[2];
int a[N], f[N][2], sm[2][2], s[N];
map<int, pair<int, int> >mp;
signed main(){
cin >> T;
F(o, 1, T){
mp.clear();
cin >> n;
F(i, 1, n) {
cin >> a[i];
s[i] = s[i - 1] ^ a[i];
if(!mp.count(s[i])) mp[s[i]] = {0, 0};
}
f[0][1] = 1;
mp[0] = {0, 0};
sm[0][0] = sm[1][0] = sm[1][1] = 0;
sm[0][1] = 1;
sum[0] = sum[1] = 0;
int pos = 0;
F(i, 1, n){
if(a[i] == 1){
(f[i][0] += (sum[1] - mp[s[i]].second) + mp[s[i]].first + mod) %= mod;
(f[i][1] += (sum[1] - mp[s[i]].second) + mp[s[i]].first + mod) %= mod;
if(i & 1){
(f[i][0] += sm[0][1] + sm[1][0]) %= mod;
(f[i][1] += sm[1][1] + sm[0][0]) %= mod;
}
else{
(f[i][0] += sm[1][1] + sm[0][0]) %= mod;
(f[i][1] += sm[0][1] + sm[1][0]) %= mod;
}
pos = min(pos, i);
(sm[i & 1][0] += f[i][0]) %= mod;
(sm[i & 1][1] += f[i][1]) %= mod;
}
else{
F(j, pos, i - 1) {
(mp[s[j]].first += f[j][0]) %= mod;
(mp[s[j]].second += f[j][1]) %= mod;
(sum[0] += f[j][0]) %= mod;
(sum[1] += f[j][1]) %= mod;
}
(f[i][0] += (sum[1] - mp[s[i]].second) + mp[s[i]].first + mod) %= mod;
(f[i][1] += (sum[1] - mp[s[i]].second) + mp[s[i]].first + mod) %= mod;
(mp[s[i]].first += f[i][0]) %= mod;
(mp[s[i]].second += f[i][1]) %= mod;
(sum[0] += f[i][0]) %= mod;
(sum[1] += f[i][1]) %= mod;
sm[0][0] = sm[0][1] = sm[1][0] = sm[1][1] = 0;
pos = N;
}
}
cout << f[n][0] << ' ' << f[n][1] << '\n'; // Alice 要拿最后一个赢,因此是正常的Nim游戏
F(i, 1, n) f[i][0] = f[i][1] = 0;
}
return fflush(0), 0;
}