[HNOI2011]卡农
省队集训的时候遇到一个加强版,推了很久的集合容斥之后自闭了
考虑递推,设\(f_i\)表示选择\(i\)个\([1,2^n-1]\)里的数,且异或和为\(0\)的排列数,答案就是\(\frac{f[m]}{m!}\)
考虑转移,发现不好转移,于是考虑在转移的时候正难则反一下
我们现在需要求\(f_i\),由于现在的异或和是\(0\),那么前\(i-1\)个异或起来一定不是\(0\),前\(i-1\)个有\(A_{2^n-1}^{i-1}\)种方案,减掉异或和为\(0\)的\(f_{i-1}\)种,那么剩下的异或和肯定不是\(0\)
现在直接选择一个当前的异或和使得当前的异或和为\(0\)就好了,但是这样可能就会出现最后选择的这个数在之前已经出现过了,于是我们还要消去一些东西
考虑连续选择两个相同的数异或和不变,于是考虑从\(f_{i-2}\)转移
我们直接从\(f_{i-2}\)的基础上选择两个一样的数,这样的选择有\(2^n-1-(i-2)\)种,其中一个需要在最后一个,另外的一个需要插到前面的\(i-2\)个数里,也就是\(i-1\)个空位中
于是\(f_i=A_{2^n-1}^{i-1}-f_{i-1}-(i-1)(2^n-i+1)f_{i-2}\)
代码
#include <bits/stdc++.h>
#define re register
const int mod = 100000007;
int n, m, now, k, pw, fac;
int f[1000005];
inline int ksm(int a, int b) {
int S = 1;
for (; b; b >>= 1, a = 1ll * a * a % mod)
if (b & 1)
S = 1ll * S * a % mod;
return S;
}
int main() {
scanf("%d%d", &n, &m);
pw = 1;
f[1] = 0, f[0] = 1;
pw = ksm(2, n) - 1;
now = pw, k = pw, fac = 1;
for (re int i = 2; i <= m; i++) {
f[i] =
((now - f[i - 1] + mod) % mod - 1ll * f[i - 2] * (i - 1) % mod * (k - i + 2) % mod + mod) % mod;
pw--;
now = 1ll * now * pw % mod;
fac = 1ll * fac * i % mod;
}
printf("%d\n", 1ll * f[m] * ksm(fac, mod - 2) % mod);
return 0;
}