P8340 [AHOI2022] 山河重整 题解
题目描述
求有多少个 \(\{1,2,\cdots,n\}\) 的子集 \(S\) 满足, \(\forall 1\le x\le n\) ,存在某个 \(S\) 的子集的和为 \(x\) ,对输入的 \(m\) 取模。
数据范围
- \(1\le n\le 5\cdot 10^5,2\le m\le 1.01\cdot 10^9\) 。
时间限制 \(\texttt{3s}\) ,空间限制 \(\texttt{1GB}\) 。
分析
参考了这篇题解。
注意到 \(S\) 符合要求的充要条件: \(\forall 1\le i\le n\) , \(\sum\limits_{x\le i,x\in S}x\ge i\) 。
必要性显然,下面证明充分性:
将 \(S\) 中元素从小到大排列: \(1=a_1\lt a_2\lt\cdots\) 。记 \(s_i=\sum_{j=1}^ia_j\) 。
对 \(k\) 归纳证明,前 \(k\) 个元素可以表示 \([1,s_k]\) 中所有数。
假设命题对 \(k-1\) 成立,对 \(k\) ,在条件中取 \(i=a_k-1\) ,有 \(s_{k-1}\ge a_k\) ,因此 \(s_k\le 2\cdot s_{k-1}\) 。
若 \(x\le s_{k-1}\) ,仅用前 \(k-1\) 个数就可以表示 \(x\) 。
若 \(s_{k-1}\lt x\le s_k\) ,则 \(s_k-x\le s_{k-1}\) ,用前 \(k-1\) 个数可以表示 \(s_k-x\) ,从 \(\{a_1,\cdots,a_k\}\) 中将表示 \(s_k-x\) 的数删去即可。
据此可以设计一个 \(\mathcal O(n^2)\) 的 \(\texttt{dp}\) :\(f_{i,j}\) 表示考虑 \(\le i\) 的数,和为 \(j\) 的方案数。可以获得 \(60pts\) 。
但很可惜,这个做法和正解没啥关系。
记 \(f_i\) 为从 \(1\sim i\) 中选择若干数,和为 \(i\) ,并且 \(\forall j\le i\) 都能被表示的方案数。
容斥,枚举最小不能被表示的数 \(i+1\) ,则 \(ans=2^n-\sum_{i=0}^{n-1}f_i2^{n-i-1}\) 。
计算 \(f_n\) 时再用一次容斥,答案为 "将 \(n\) 划分为若干数之和的方案数" 减去 "不能表示 \([1,n]\) 所有数的方案数" 。
对于前者,可以看成有价值为 \(1,2,\cdots,n\) 的物品各一个,然后跑 \(01\) 背包。
直接跑是 \(\mathcal O(n^2)\) 的,但是注意到物品价值互不相同,所以选择的物品个数不超过 \(\mathcal O(\sqrt n)\) 。
我们需要统计面积为 \(n\) ,长为 \(n\) 但宽为 \(\mathcal O(\sqrt n)\) 的阶梯数, \(01\) 背包在按列 \(\texttt{dp}\) ,现在转换思路按行 \(\texttt{dp}\) 。
将每行格子数看成物品价值 \(\le\mathcal O(\sqrt n)\) ,并且这个物品可以选择任意多个(完全背包)。限制为若最大物品价值为 \(i\) ,那么每种价值 \(\lt i\) 的物品都至少取一个。
记 \(dp_{i,j}\) 表示考虑价值 \(\ge i\) 的物品,已经花费了 \(j\) 的容量的方案数。
- 先强制选一个价值为 \(i\) 的物品: \(dp_{i+1,j}\to dp_{i,j+i}\) 。
- 然后对价值为 \(i\) 的物品跑完全背包: \(dp_{i,j}\to dp_{i,j+i}\) 。
注意 \(2\) 步转移有先后顺序,而且可以用滚动数组优化。
对于后者,记 \(g_n\) 为选择和为 \(n\) 的若干数,且不能表示 \([1,n]\) 的方案数。
枚举 \(j+1\) 为最小不能表示的数,于是 \(g_n=\sum_{j=0}^{n-1}f_jh_{n,j}\) ,其中系数 \(h_{n,j}\) 为在 \([j+2,\infty]\) 中选择若干个数,和为 \(n-j\) 的方案数。
枚举选了 \(i\) 个数,先把所有数减去 \(j+1\) ,则我们需要从 \(N^*\) 中选出和为 \(n-i\cdot (j+2)\) 的数。
这个状态的定义和 \(f\) 很像,只不过额外有选数个数限制。
注意到 "钦定选 \(i\) 个数" 在转换后的 \(\texttt{dp}\) 中等价于 "钦定最大物品价值为 \(i\) " ,因此 \(h_{i,j}\) 可以通过只跑 \(dp\) 数组的最后 \(i\) 层转移得到。
带回到 \(g\) 数组,我们只需要倒序枚举 \(i\) ,在跑第 \(i\) 层转移之前,将 \(f_j\) 的贡献统计到 \(g_{j+i\cdot(j+2)}\) 位置,然后对 \(g\) 跑转移。
于是出现了一种非常有意思的情形,类似半在线卷积:
- \(f\) 有初始值,但是要减去 \(g_i\) 才能得到真正的 \(f_i\) 。
- 求解 \(g_i\) 需要用到 \(f_1\sim f_{i-1}\) 的值。
Key observation:根据 \(h_{i,j}\) 的定义,若 \(h_{i,j}\) 非零,则 \(j+2\le i-j\) ,因此 \(j\le\lfloor\frac i2\rfloor\) 。
换言之, \(g_i\) 只依赖于 \(f_1\sim f_{\lfloor\frac i2\rfloor}\) 的值!
考虑倍增,假设我们已经知道 \(f_1\sim f_{\lfloor\frac n2\rfloor}\) 的值,需要求 \(f_1\sim f_n\) 。
先按前面讲的内容求出 \(g_1\sim g_n\) , \(f\) 就迎刃而解了。
时间复杂度 \(T(n)=T(\frac n2)+O(n\sqrt n)\) ,解得 \(T(n)=O(n\sqrt n)\) 。
本题轻微卡常,稍微精细实现一下,再加个取模优化就可以过了。
#include<bits/stdc++.h>
using namespace std;
const int lim=1000,maxn=5e5+5;
int n,cur=1,mod,res;
int f[maxn],g[maxn];
void add(int &x,int y)
{
if((x+=y)>=mod) x-=mod;
}
/// f[i] 表示和为 i 且能表示 [1,i] 的方案数,初始 dp 仅考虑第一个条件,容斥需要减掉 g[i]
/// g[i] 表示和为 i 且不能表示 [1,i] 的方案数
void solve(int n)
{
if(n==0) return f[0]=1,void();
solve(n/2);///已知 f[1~n/2] 的值,用于求 g
for(int i=1;i<=n;i++) g[i]=0;
for(int i=lim;i>=1;i--)
{
if(i*(i+1)/2>n) continue;
for(int j=n;j>=i;j--) g[j]=g[j-i];
for(int j=0;j+i*(j+2)<=n;j++) add(g[j+i*(j+2)],f[j]);///本质上是执行了 f[j]*h[n][j] -> g[n] 的转移
for(int j=i;j<=n;j++) add(g[j],g[j-i]);
}
for(int i=n/2+1;i<=n;i++) add(f[i],mod-g[i]);///更新 f[n/2+1~n]
}
int main()
{
scanf("%d%d",&n,&mod);
for(int i=lim;i>=1;i--)
{
for(int j=n;j>=i;j--) f[j]=f[j-i];///强制选一个价值为 i 的物品
f[i]=1;///若选择的物品的最大价值为 i ,赋初始值
for(int j=i;j<=n;j++) add(f[j],f[j-i]);///对价值为 i 的物品跑完全背包
}
solve(n);
for(int i=n-1;i>=0;i--) res=(res-1ll*f[i]*cur)%mod,cur=2*cur%mod;///容斥减去 f[i]*2^(n-i-1)
add(res,cur),add(res,mod);///加上总方案数 2^n
printf("%d\n",res);
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/16478847.html
浙公网安备 33010602011771号