P6442 [COCI2011-2012#6] KOŠARE
written on 2022-07-08
一开始看到 \(m\) 的数据范围,很明显一眼状压。但是 \(n\) 的范围较大,因此暴力的一个一个箱子进行状压转移只能拿到 \(50pts\)。对这种需要顺序枚举 \(n\) 的做法,显然优化的余地已经很小了,因此我们考虑换一种思考方式。
首先对原题进行转化,即为 选取若干个二进制数,使他们或起来为全集 \(s\)。
进一步对问题进行抽象,设 \(f_i\) 表示 选取若干个数或起来为状态 \(i\) 的方案数,用 dp 的方法进行转移,最后要求解的答案就是 \(f_s\)。如何计算 \(f_i\) 呢?这里就需要用到容斥进行巧妙地转移,我们设 \(g_i\) 表示选取若干个数或起来为状态 \(i\) 的子集的方案数,那么很显然,假如 \(i\) 有 \(cnt_i\) 个子集(这里将 \(0\)(空集) 也视为子集),那么 \(g_i=2^{cnt_i}\),也就是其中这 \(cnt_i\) 个元素任意选或不选的结果,那么要求解 \(f_i\) 时只要乘以对应的容斥系数即可。特殊地,这题的容斥系数也就是以 “\(i\) 在二进制下与 \(s\) 相差的 \(1\) 的个数”为 \(-1\) 指数的那个数。
\(cnt\) (代码中为 \(f\))在读入时初始化,然后子集转移时用高维前缀和(sosdp)即可。时间复杂度 \(O(n+m\times 2^m)\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
int n,m;
ll f[(1<<20)+5],ans;
ll two[1000005];
int count_1(int x)
{
int cnt=0;
while(x)
{
x&=(x-1);
cnt++;
}
return cnt;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int num,x,s=(1<<m)-1;
scanf("%d",&num);
while(num--)
{
scanf("%d",&x);
s^=(1<<(x-1));
}
f[s]++;
}
for(int i=0;i<m;i++) for(int s=(1<<m)-1;s>=0;s--) if(s&(1<<i)) f[s^(1<<i)]=(f[s^(1<<i)]+f[s])%mod;
two[0]=1;
for(int i=1;i<=n;i++) two[i]=two[i-1]*2%mod;
for(int i=0;i<(1<<m);i++)
{
int cnt=count_1(i);
if(cnt%2==0) ans=(ans+two[f[i]])%mod;
else ans=((ans-two[f[i]])%mod+mod)%mod;
}
printf("%lld\n",ans);
}