博弈论做题记录
AGC010F Tree Game
令 \(a[u]\) 是节点 \(u\) 上的石子数。
感性理解一下:如果当前节点 \(u\) 以及它的唯一子节点 \(v\), 满足 \(a[u] \le a[v]\),那么如果先手向下到 \(v\),后手可以向上走到 \(u\),先手就会被硬控住,导致直接死掉。
所以我们可以猜出一个结论:从一个节点走到 \(a\) 值比他更大的节点是不优的。
(然鹅我是做到这里就不会了e)
首先枚举先手把棋子放在哪里,并以该点作为树的根。
由于我们只关心先手赢还是后手赢,所以我们可以令 \(f[u] = 0/1\) 为在只考虑 \(u\) 为根的子树中,且棋子刚好是放在 \(u\) 上时,是先手必胜/必败。
那么 \(f[u] = 1\) 当且仅当有某一个 \(u\) 的子节点 \(v\) 满足 \(a[v] < a[u]\) 且 \(f[v] = 0\).
因为此时先手移动到 \(v\) 上后,后手有两种情况:
-
向上走到 \(u\) :此时先手再向下走即可。
-
向下走:由于 \(f[v] = 0\),所以必输。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3000 + 10;
int n, a[N], f[N];
struct edge{
int v, next;
}edges[N << 1];
int head[N], idx;
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
bool dfs(int u, int fa){
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v;
if(v == fa || a[v] >= a[u]) continue;
f[u] &= dfs(v, u);
}
f[u] ^= 1;
return f[u];
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
add_edge(x, y); add_edge(y, x);
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++) f[j] = 1;
if(dfs(i, 0)) cout << i << " ";
}
return 0;
}
AGC002E Candy Piles
很牛的一道题。
首先按 \(a\) 从大到小排序。那么第二个操作就转化为将第一堆石子移去。
考虑转化问题:我们将石子从下到上一个个排出来。
如:
6 4 3 2 2 1
每一堆拆成一颗颗石子的样子:
1
1
1 1
1 1 1
1 1 1 1 1
1 1 1 1 1 1
我们发现,第一种操作就等价于抽走最下面的一行,第二种操作等价于抽走最左边的一列。
再次进行转化:我们令一个棋子放在 \((1,1)\) 的位置,此时 第一种操作等价于棋子向上走,第二种操作等价于向右走。
我们考虑 \((1,1)\) 位置是先手必胜还是后手必胜。将每个位置的状态算出来,观察后可以猜出一个性质:在同一条对角线的位置的状态是一样的。 证明也十分简单,直接反证法即可。
于是 \((1,1)\) 的状态就跟 \((x,x)\) 的一样了,于是我们找到一个最大的 \(i\) 使得 \(a_i \ge i\)。由于 \((1, 1)\) 的状态跟 \((i,i)\) 一样,问题再次转化为:判断 \((i,i)\) 的胜负状态。棋子在 \((i,i)\) 上时,先手只有两种情况:
-
向上走:先后手交替移动,共移动 \(a_i - i\) 步,显然当 \(a_i - i\) 是奇数时先手必胜。
-
向右走:先后手类似的移动,我们令 \(j\) 是满足 \(a_j = i\) 最大的一个,其实就是棋子最远能到的位置。移动了 \(j - i\) 步,显然 \(a_j - i\) 时先手必胜。
只要满足上面的某一种情况,那么 \((i,i)\) 就是先手必胜,即 \((1,1)\) 是先手必胜。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, a[N];
bool cmp(int x, int y){return x > y;}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; i++){
if(a[i + 1] < i + 1){
bool up = (a[i] - i) & 1, right = false;
for(int j = i + 1; a[j] == i; j++) right ^= 1;
if(up || right) cout << "First";
else cout << "Second";
break;
}
}
return 0;
}
P5363 [SDOI2019] 移动金币
不错的一道题。
不难看出题目中除最后一枚金币外,移动金币不会影响左右两枚金币之间的距离。进一步的,我们可以只关注移动前后金币之间的距离变化情况。
为了更加形象,可以将金币之间的距离看成一堆堆石子,从右到左编号。 特殊的,最后的一枚金币与棋盘右端点的距离定义为第 \(0\) 堆石子的数量。
容易发现现在游戏就等价为阶梯 \(\rm Nim\) 游戏了。阶梯 \(\rm Nim\) 游戏先手必胜局面必然满足奇数堆石子异或和不等于 \(0\)。考虑这个问题的补集,即计算奇数堆石子异或和等于 \(0\) 的局面个数。
这可以通过一个背包在 \(O(n^2m)\) 时间计算出,但这还不够。
注意到位运算不进位的性质,我们分别计算每一位的贡献。 令 \(f_{i,j}\) 为考虑前 \(i\) 位,且已经分配了 \(j\) 个石子后且奇数堆石子异或和等于 \(0\) 的情况个数。
考虑第 \(i\) 位有多少堆石子个数二进制上为 \(1\),由于异或和等于 \(0\),肯定只有偶数堆石子个数第 \(i\) 个二进制位为 \(1\)。了解这一点后,就容易得到:(其中 \(cnt1\) 为有多少编号是奇数的堆)
最后统计答案,先对偶数堆做一下插板,然后再用总方案数减一下即可。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5 + 10, mod = 1e9 + 9;
int n, m, f[35][N];
int jc[N], jcinv[N];
int qpow(int x, int y){
x %= mod; int ret = 1;
while(y){
if(y & 1) ret = (ret * x) % mod;
x = (x * x) % mod;
y >>= 1;
}
return ret;
}
int C(int x, int y){
return ((jc[x] * jcinv[y] % mod) * jcinv[x - y]) % mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m; jc[0] = jcinv[0] = 1;
for(int i = 1; i < N; i++) jc[i] = (jc[i - 1] * i) % mod, jcinv[i] = qpow(jc[i], mod - 2);
f[0][0] = 1;
int cnt1= (m + 1) / 2, cnt0 = m - cnt1 + 1;
for(int i = 1; i < 30; i++){
for(int j = 0; j <= n - m; j++){
for(int k = 0; k <= cnt1 && ((k * (1 << (i - 1))) <= j); k += 2) f[i][j] = (f[i][j] + f[i - 1][j - k * (1 << (i - 1))] * C(cnt1, k)) % mod;
// cout << i << " " << j << " " << f[i][j] << "\n";
}
}
int ans = 0;
for(int i = 0; i <= n - m; i++) ans = (ans + (f[29][i] * C(n - m - i + cnt0 - 1, cnt0 - 1)) % mod) % mod;
cout << (C(n, m) - ans + mod) % mod << "\n";
return 0;
}
P2490 [SDOI2011] 黑白棋
跟上一题很像,只是变成了 \(\rm K-Nim\)。
CF794E Choosing Carrot
还不会