博弈论做题记录

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\) 为有多少编号是奇数的堆)

\[f_{i,j} = \sum_{k=0}^{k\le cnt1 且 k \times 2^i \le n - m}f_{i-1, j- k\times2^i} \times C_{cnt1} ^ k \]

最后统计答案,先对偶数堆做一下插板,然后再用总方案数减一下即可。

点击查看代码
#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

还不会

posted @ 2024-04-28 13:31  Little_corn  阅读(13)  评论(0编辑  收藏  举报