正题
题目链接:https://www.luogu.com.cn/problem/P8340
题目大意
给出一个\(n\)和模数\(P\)。求有多少个在\(1\sim n\)中选择若干个数的集合\(S\),满足\(1\sim n\)中的每个数都可以表示成\(S\)的某个子集的和。
\(1\leq n\leq 5\times 10^5,2\leq P\leq 1.1\times 10^9\)
解题思路
先考虑集合\(S\)符合要求的条件。
我们从小到大加入\(S\)集合中的数,那么假设我们目前能表示\(1\sim k\),那么在我们加入一个数字\(x\)时,显然要求\(x\leq k+1\),否则\(k+1\)就无法被表示。那么我们在加入\(x\)后我们能表示的范围就变成了\(1\sim x+k\)。
那么这个条件已经很显然,对于任意的\(i\in[1,n]\),要求\(\leq i\)的数字和\(\geq i\)即可。
考虑减去不合法的方案,记\(f_i\)表示恰好在位置\(i\)处\(\leq i\)的数字和为\(i\)且前面的都合法,那么我们不选\(i+1\)即可。
那么这个\(f_i\)怎么计算,因为要求前面的位置都合法,那么我们继续考虑减去不合法的方案。我们先算出\(i\)的整数划分,然后不合法的方案我们考虑枚举第一个不合法的位置。和上面的类似,我们枚举一个\(j\),然后前面的方案就是\(f_j\),之后\(j+1\)不选,那么剩下\(j+2\sim i\)中选择若干个数使得其和\(j\)的和为\(i\)。
先考虑整数拆分的方案,这个好说,因为划分的每个数字都要求不同,所以数字总数是\(\sqrt n\)级别的。那么这就很好解决了,我们设\(g_{i,j}\)表示目前和为\(i\),选了\(j\)个数字,那么我们每次要么多一个数字,要么所有的数字一起加\(1\)就好了。
然后考虑后面那个转移,因为\(f_j+sum=i,sum\geq j+2\),也就是说\(2j+2\leq i\)。那么我们可以考虑一个倍增的做法,每次先处理\(1\sim i\)然后再处理\(1\sim 2i\)。
之后考虑怎么快速计算左边对右边的贡献,考虑用上面类似的方法,不过我们再每次处理完后要令\(g_{j+(j+2)\times i}+=f_j\)。
时间复杂度:\(O(n\sqrt n)\)
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll N=5e5+10;
ll n,P,h[N],f[N];
void add(ll &x,ll y)
{x=((x+y<P)?(x+y):(x+y-P));}
signed main()
{
scanf("%lld%lld",&n,&P);
for(ll i=n;i>=1;i--){
if(i*(i+1)/2>n)continue;
for(ll j=n;j>=i;j--)f[j]=f[j-i];
f[i]++;
for(ll j=i;j<=n;j++)add(f[j],f[j-i]);
}
memset(h,0,sizeof(h));f[0]=1;
for(ll l=1,r;l<n;l=r){
r=min(l*2+2,n);
for(ll i=l;i>=1;i--){
if(i*(i+1)/2>r)continue;
for(ll j=r;j>=i;j--)h[j]=h[j-i];
for(ll j=0;j+(j+2)*i<=r;j++)add(h[j+(j+2)*i],f[j]);
for(ll j=i;j<=r;j++)add(h[j],h[j-i]);
}
for(ll i=l+1;i<=r;i++)add(f[i],P-h[i]);
for(ll i=1;i<=r;i++)h[i]=0;
}
ll ans=1;
for(ll i=1;i<=n;i++)ans=ans*2ll%P;
for(ll i=n-1,pw=1;i>=0;i--,pw=pw*2ll%P)
add(ans,P-pw*f[i]%P);
printf("%lld\n",ans);
return 0;
}