『容斥·状压』CF449D Jzzhu and Numbers

P r o b l e m \mathrm{Problem} Problem

给出一个长度为n的序列 a 1 , a 2 . . . a n a_1,a_2...a_n a1,a2...an

求构造出一个序列 i 1 ≤ i 2 ≤ . . . ≤ i k ( 1 ≤ k ≤ n ) i_1 \le i_2 \le ... \le i_k(1\le{k}\le{n}) i1i2...ik(1kn)使得 a i 1   a n d   a i 2   a n d   . . .   a n d   a i k = 0 a_{i_1}\mathrm{\ and\ }a_{i_2}\mathrm{\ and\ }...\mathrm{\ and}\ a_{i_k}=0 ai1 and ai2 and ... and aik=0

求方案数模 1 0 9 + 7 10^9+7 109+7

也就是从 { a i } \{a_i\} {ai} 里面选出一个非空子集使这些数按位与起来为 0 0 0.


S o l u t i o n \mathrm{Solution} Solution

前置芝士: n n n个数中,任意 i i i 有几个数满足 a i   a n d   i = i a_i\ \mathrm{and\ } i=i ai and i=i

考虑状压: f i ∑ j ∉ i f i + 2 j f_i\sum _{j∉i}f_{i+2^j} fij/ifi+2j

这样我们发现会有重复,考虑为什么会重复。
在这里插入图片描述

那么怎么办呢?首先想到的容斥,即:加上新增 1 1 1 的,减去新增 2 2 2 的…

但是我们神奇的发现将 i i i j j j 的枚举顺序调换一下,就神奇的不重复了。

在这里插入图片描述

因此我们得到了一个做状压计数的结论:

  • 若某一个数想要求得以它为子集的数的所有贡献,我们只需要调换枚举顺序,累计每一次新增 1 1 1 的贡献即可。

回到正题,那么这题运用上述结论就十分好解决了。

我们设 f i f_i fi 表示and值为 i i i 的答案,我们发现十分难搞。但是令 f i f_i fi表示and值子集为 i i i 的方案数,我们发现我们可以用 f i f_i fi 减去以 i i i 的贡献即为正确答案。

类比上面的前置芝士,我们发现我们只要将加号改成减号就可以得到答案。

那么我们才能得到 f i f_i fi 呢,我们发现只要有 n n n 个数的子集存在 i i i,答案即为: 2 n − 1 2^n-1 2n1

因此我们先令上述统计个数一遍,在变换成方案数以后再相减一遍即可。


C o d e \mathrm{Code} Code

#include <bits/stdc++.h>
#define int long long

using namespace std;
const int N = 3e6;
const int P = 1e9 + 7;

int n, S;
int f[N];

int read(void)
{
	int s = 0, w = 0; char c = getchar();
	while (!isdigit(c)) w |= c == '-', c = getchar();
	while (isdigit(c)) s = s*10+c-48, c = getchar();
	return w ? -s : s;
}

int power(int a, int b) {
	int res = 1;
	while (b > 0) {
		if (b & 1) res = res * a % P;
		a = a * a % P, b >>= 1;
	}
	return res;
}

void DP(int x)
{
	for (int j=0;j<=19;++j)
	{
		for (int i=S-1;i>=0;--i)
			if (((i >> j) & 1) == 0) 
				f[i] = (f[i] + f[i | 1 << j] * x) % P;
	}
	return; 
}

signed main(void)
{
	n = read(), S = (1 << 20) - 1;
	for (int i=1;i<=n;++i) f[read()] ++;
	DP(1);
	for (int i=0;i<=S;++i) 
		f[i] = power(2, f[i]) - 1;
	DP(-1); 
	cout << (f[0] + P) % P << endl;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值