nssl 1522.简单数数题

D e s c r i p t i o n Description Description

给定一个大小为 n n n的可重集合,求这些集合的子集的所有子集的和(这里的子集都可重)能被 m m m整除的子集有多少个

数据范围:nssl 1522.简单数数题_第1张图片


S o l u t i o n Solution Solution

显然一个有 n n n个元素的可重集,设所有元素的和为 s u m sum sum,则它所有子集的和为 2 n − 1 × s u m 2^{n-1}\times sum 2n1×sum

证明:
考虑每个元素对子集和的贡献,假设一个元素 x x x选了,那么无论其它元素选不选,它都有 x x x的贡献,而其它选择的方案有 2 n − 1 2^{n-1} 2n1种( n − 1 n-1 n1个数可选可不选),那么它的贡献就是 x × 2 n − 1 x\times 2^{n-1} x×2n1
则总贡献即 ∑ x × 2 n − 1 = s u m × 2 n − 1 \sum x\times 2^{n-1}=sum\times 2^{n-1} x×2n1=sum×2n1

这个结论有什么用呢?
这告诉我们,集合的个数无论多少,都仅只会多产生2的因子
举个栗子,假如一个子集的大小为 n n n,那么实际上会多出 n − 1 n-1 n1个2这个因子

其实也就相当于如果一个集合的大小为 n n n,它的元素和为 s u m sum sum,显然它的子集和为 2 n − 1 × s u m 2^{n-1}\times sum 2n1×sum

  1. 如果 m m m转换为二进制后后面至少有 n − 1 n-1 n1个0,即它含有 2 n − 1 2^{n-1} 2n1这个因子
    根据之前的结论,那么有 s u m × 2 n − 1 m o d    m = s u m m o d    m 2 n − 1 sum\times 2^{n-1} \mod m=sum\mod \frac m{2^{n-1}} sum×2n1modm=summod2n1m
  2. 如果 m m m转换为二进制后面0不够多,设它的0有 l e n len len个(这个可以用 l o w b i t lowbit lowbit求出来),那么对于每个大小为 j j j的集合,如果 j ≥ l e n j\geq len jlen,则它的选取方案是不受 j j j的影响的

换句话说,我们只需保存 l e n len len以内的答案,多于 l e n len len的都把它存到 l e n len len里面(因为如果 j ≥ l e n j\geq len jlen,则题目跟背包没有区别)

所以,设 f i , j , k f_{i,j,k} fi,j,k表示前 i i i个数,选了 j j j个数, m o d mod mod缩小后的 m m m的余数是 k k k的方案数
滚动后直接转移即可

由于 j j j的变大, k k k的上界会相对应的缩小,所以总的复杂度是 O ( n m ) O(nm) O(nm),需要适当卡常

T i p s : Tips: Tips:
由于大小为 n n n的集合对 m m m的影响却是 2 n − 1 2^{n-1} 2n1,所以我们把 m m m乘2,这样方便转移


C o d e Code Code

#pragma GCC optimize(2)
%:pragma GCC optimize(3)
%:pragma GCC optimize("Ofast")
%:pragma GCC optimize("inline")
#include
#include
#include
#include
#define LL long long
#define mod 1000000007
using namespace std;int n,m,a[5010],len,f[2][14][10010],nmd,Ans;
inline LL read()
{
     
	char c;LL d=1,f=0;
	while(c=getchar(),!isdigit(c)) if(c=='-') d=-1;f=(f<<3)+(f<<1)+c-48;
	while(c=getchar(),isdigit(c)) f=(f<<3)+(f<<1)+c-48;
	return d*f;
}
signed main()
{
     
	n=read();m=read()*2;
	int x=m;
	while(!(x&1)) len++,x>>=1;
	f[0][0][0]=1;
	for(register int i=1;i<=n;i++)
	{
     
		a[i]=read();
		for(register int j=0;j<=len;j++) 
		{
     
			nmd=m/(1<<j);
			for(register int k=0;k<nmd;k++) f[i&1][j][k]=f[i&1^1][j][k];//初始化,不能用memset,不然会T
		}
		for(register int j=0;j<=len;j++)
		{
     
			nmd=m/(1<<j);//nmd=new mod
			for(register int k=0;k<nmd;k++)
			{
     
				if(j==len) //>=len的部分都转移到len来
				{
     
					f[i&1][len][(k+a[i])%nmd]+=f[i&1^1][len][k];
					if(f[i&1][len][(k+a[i])%nmd]>mod) f[i&1][len][(k+a[i])%nmd]-=mod;
				}
				else 
				{
     
					(f[i&1][j+1][(k+a[i])%(nmd/2)]+=f[i&1^1][j][k])%=mod;//注意每多放一个数,模数都要除以二
					if(f[i&1][j+1][(k+a[i])%(nmd/2)]>mod) f[i&1][j+1][(k+a[i])%(nmd/2)]-=mod;
				}
			}
		}
	}
	for(register int i=1;i<=len;i++) (Ans+=f[n&1][i][0])%=mod;
	printf("%d",Ans);
}

你可能感兴趣的:(nssl,1522,简单数数题)