2020CCPC威海 J - Steins;Game (sg函数、线性基)

题意:n堆石子,每堆石子有黑和白两种颜色,拿黑色堆石子的时候必须拿数量最少的(最少一个),白色没有限制可以任意拿(最少一个)。后手的人可以在游戏开始之前对所有石子堆染色,问后手能赢的染色方案数。

题解:首先可以将黑色和白色分成两个游戏,两个游戏的异或和为0即后手胜利。

白色游戏:相当于nim游戏

黑色游戏:通过sg打表能发现

SG值 = 最小堆石子数 − ( ( 最小堆数量 + [ 所有堆石子数相同 ] ) % 2 )

能发现sg值只跟最小堆有关系,然后我们可以枚举最小堆。用一个pre维护异或前缀和,suf维护异或后缀和,从后往前维护一个线性基。由于我们枚举到的堆是最小堆,所以石子数量比当前枚举的堆要少的一定是白色,我们要最后总的异或值为0,那么要看线性基里能不能表示出满足异或和为0的数,方案即 2的(s - tot)次方。最后再乘上一个组合数即可。具体细节代码可见。

sg函数打表代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 110;
int sg[N];

int SG(priority_queue<int, vector<int>, greater<int> > q) {
	bool vis[N] = { false };
	if (q.empty()) return 0;
	int t = q.top();
	q.pop();
	vis[SG(q)] = true;
	for (int i = 1; i < t; i++) {
		auto tmp = q;
		tmp.push(t - i);
		vis[SG(tmp)] = true;
	}
	for (int i = 0; ; i++)
		if (!vis[i]) return i;
}

int main() {
	priority_queue<int, vector<int>, greater<int> > q;
	for (int i = 0; i <= 5; i++) {
		for (int j = i; j <= 5; j++) {
			for (int k = j; k <= 5; k++) {
				for (int l = k; l <= 5; l++) {
					for (int r = l; r <= 5; r++) {
						while (!q.empty()) q.pop();
						if (i) q.push(i), cout << i << " ";
						if (j) q.push(j), cout << j << " ";
						if (k) q.push(k), cout << k << " ";
						if (l) q.push(l), cout << l << " ";
						if (r) q.push(r), cout << r << " ";
						cout << " : " << SG(q) << endl;
					}
				}
			}
		}
	}
	return 0;
}

题目代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 1e5 + 10;
int f[N], inv[N];
int a[N], pre[N], suf[N];
int same[N], num[N];
map<int, int> t;
struct LB {
	const static int N = 64;
	ll a[N + 1], tot;
	LB() { memset(a, 0, sizeof(a)), tot = 0; }
	bool insert(ll x) {
		for (int i = N; i >= 0; i--) {
			if (x & (1ll << i)) {
				if (a[i]) x ^= a[i];
				else {
					a[i] = x;
					tot++;
					break;
				}
			}
		}
		return x > 0;
	}
	bool count(ll x) {
		for (int i = N; i >= 0; i--)
			if (x & (1ll << i)) x ^= a[i];
		return x == 0;
	}
}LB;

int qpow(int a, int b) {
	int res = 1;
	while (b) {
		if (b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

void init(int n) {
	f[0] = inv[0] = 1;
	for (int i = 1; i <= n; i++)
		f[i] = f[i - 1] * i % mod;
	inv[n] = qpow(f[n], mod - 2);
	for (int i = n - 1; i >= 1; i--)
		inv[i] = inv[i + 1] * (i + 1) % mod;
}

int C(int a, int b) {
	if (a < b) return 0;
	return f[a] * inv[b] % mod * inv[a - b] % mod;
}

signed main() {
	init(N - 1);
	int n;
	cin >> n;
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		t[a[i]]++;
		ans ^= a[i];
	}
	ans = (ans == 0 ? 1 : 0);
	sort(a + 1, a + 1 + n);
	for (int i = 1; i <= n; i++)
		pre[i] = pre[i - 1] ^ a[i];
	for (int i = n; i >= 1; i--)
		suf[i] = suf[i + 1] ^ a[i];
	for (int i = n; i >= 1; i--) {
		same[i] = (a[i] == a[i + 1] ? same[i + 1] + 1 : 1);
		num[i] = t[a[i]];
	}
	int head = n + 1;
	for (int i = n; i >= 1; i--) {
		int sum = 0;//记录方案
		int x = pre[i - 1];//前缀异或和
		int y = a[i] - ((same[i] + 1) & 1);
		//当前选了same[i]个数量为a[i]的石堆染成黑色(所有石堆石子数相同)
		int z = suf[i + same[i]];//后缀异或和
		if ((x ^ y ^ z) == 0) sum = (sum + 1) % mod;//如果异或和为0,方法数加一
		y = a[i] - (same[i] & 1);//同上y,但是所有石堆石子数不相同
		int need = x ^ y;//满足异或和为0的数

		if (LB.count(need)) sum = (sum + qpow(2, n - head + 1 - LB.tot)) % mod;
		//如果need能用线性基表示出来,线性基能表示出这个数的方案为2^(s - tot)
		//s为线性基插入次数,tot为线性基内的个数

		if ((need ^ suf[i + same[i]]) == 0) sum = (sum - 1 + mod) % mod;
		//如果后缀所有数异或和等于need那么方案数要减一

		if (same[i - 1] == 1) {
			//如果当前是a[i]数量的最后一个石堆,把所有值为a[i]的数插入到线性基中
			for (int j = 1; j <= same[i]; j++) LB.insert(a[i]);
			head = i;
		}
		ans = (ans + sum * C(num[i], same[i]) % mod) % mod;
		//最后要乘上在num[i]中选same[i]堆的方法数
	}
	cout << ans << endl;
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值