正题
题目链接:https://www.luogu.com.cn/problem/P8340
题目大意
给出一个nnn和模数PPP。求有多少个在1∼n1\sim n1∼n中选择若干个数的集合SSS,满足1∼n1\sim n1∼n中的每个数都可以表示成SSS的某个子集的和。
1≤n≤5×105,2≤P≤1.1×1091\leq n\leq 5\times 10^5,2\leq P\leq 1.1\times 10^91≤n≤5×105,2≤P≤1.1×109
解题思路
先考虑集合SSS符合要求的条件。
我们从小到大加入SSS集合中的数,那么假设我们目前能表示1∼k1\sim k1∼k,那么在我们加入一个数字xxx时,显然要求x≤k+1x\leq k+1x≤k+1,否则k+1k+1k+1就无法被表示。那么我们在加入xxx后我们能表示的范围就变成了1∼x+k1\sim x+k1∼x+k。
那么这个条件已经很显然,对于任意的i∈[1,n]i\in[1,n]i∈[1,n],要求≤i\leq i≤i的数字和≥i\geq i≥i即可。
考虑减去不合法的方案,记fif_ifi表示恰好在位置iii处≤i\leq i≤i的数字和为iii且前面的都合法,那么我们不选i+1i+1i+1即可。
那么这个fif_ifi怎么计算,因为要求前面的位置都合法,那么我们继续考虑减去不合法的方案。我们先算出iii的整数划分,然后不合法的方案我们考虑枚举第一个不合法的位置。和上面的类似,我们枚举一个jjj,然后前面的方案就是fjf_jfj,之后j+1j+1j+1不选,那么剩下j+2∼ij+2\sim ij+2∼i中选择若干个数使得其和jjj的和为iii。
先考虑整数拆分的方案,这个好说,因为划分的每个数字都要求不同,所以数字总数是n\sqrt nn级别的。那么这就很好解决了,我们设gi,jg_{i,j}gi,j表示目前和为iii,选了jjj个数字,那么我们每次要么多一个数字,要么所有的数字一起加111就好了。
然后考虑后面那个转移,因为fj+sum=i,sum≥j+2f_j+sum=i,sum\geq j+2fj+sum=i,sum≥j+2,也就是说2j+2≤i2j+2\leq i2j+2≤i。那么我们可以考虑一个倍增的做法,每次先处理1∼i1\sim i1∼i然后再处理1∼2i1\sim 2i1∼2i。
之后考虑怎么快速计算左边对右边的贡献,考虑用上面类似的方法,不过我们再每次处理完后要令gj+(j+2)×i+=fjg_{j+(j+2)\times i}+=f_jgj+(j+2)×i+=fj。
时间复杂度:O(nn)O(n\sqrt n)O(nn)
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;
}