给定 ,求有多少个 的子集 ,满足 ,对给定正整数 取模。
也就是求有多少个子集 满足其 01 背包可以表示出 中的所有数。
,时限 s。
首先有一个显然的充要条件:将 中元素排序后,。
那么直接得到了 做法:设 表示考虑了 ,当前 的方案数,转移是简单的。
问题是这个做法没有优化前途。
考虑容斥,钦定 作为最小的不能被表示的数,那么 要满足 且 能表示出 中的所有数。
那么只要求出 表示有多少个 满足 且其可以表示 中的所有数。
考虑容斥计算 ,先算出 的 的个数。再枚举一个 ,钦定无法表示 ,那么 对 的贡献为 ,其中 表示有多少个 满足 。
继续,容易发现当 则 ,那么此时 。故可以倍增计算 。
现在问题变成如何求 的初值以及 。
先来考虑 的初值,即求有多少个 满足 。这是一个经典问题,即互异拆分数:
考虑暴力 dp,设 表示考虑完 ,现在总和为 。这样做是 的。
考虑 Ferrers 图,即总共有 个点,每行点数递减,一行表示 中的一个数:
....... ..... ... .这个暴力 dp 相当于逐行加点,而注意到由于要求互异,即每行的点数不同,所以行数是 的。那么可以考虑逐列加点:
- 设 表示当前列有 个点,总共加入了 个点;
那么这样 dp 就变成了 的。
那么跑一遍该 dp 即可得到 的初值,而不难发现并不用显式求出 ,仅需将前一半的计算完成的 作为初值跑该 dp 即可。具体实现看代码。
那么时间复杂度为 ,代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int BS=1005,S=500005;
int n,p;
int f[S],g[S];
inline void add(int &x,int y)
{
x+=y;
if(x>=p) x-=p;
}
void slove(int n)
{
if(n==1) return;
int m=n/2;
slove(m);
for(int i=0;i<=n;i++) g[i]=0;
for(int i=BS-3;i>=1;i--)
{
for(int j=n;j>=i;j--) g[j]=g[j-i];
for(int j=0;j<i;j++) g[j]=0;
for(int j=0;j<=m&&j+(j+2)*i<=n;j++)
add(g[j+(j+2)*i],p-f[j]);
for(int j=i;j<=n;j++) add(g[j],g[j-i]);
}
for(int i=m+1;i<=n;i++) add(f[i],g[i]);
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>p;
for(int i=BS-3;i>=1;i--)
{
for(int j=n;j>=i;j--) f[j]=f[j-i];
for(int j=0;j<i;j++) f[j]=0;
add(f[i],1);
for(int j=i;j<=n;j++) add(f[j],f[j-i]);
}
f[0]=1;
slove(n);
int ans=1;
for(int i=1;i<=n;i++) ans=1ll*ans*2%p;
for(int i=n,pw2=1;i>=1;i--,pw2=1ll*pw2*2%p)
add(ans,p-1ll*pw2*f[i-1]%p);
cout<<ans<<'\n';
return 0;
}
/*
相当于背包覆盖所有点
相当于 a[i]<=(\sum a[1,i])+1
考虑容斥,数不合法的序列
最多有 log 个位置违法
* n sqrt n
* 考虑互异分拆数的 n sqrt n 做法,记录 f[i][sm] 表示这一列还有 i 个点,和为 sm
f[i][sm]->f[i][sm+i] or f[i-1][sm+i]
容斥,考虑枚举爆掉的点前一个位置的和
so hard
*/