[AHOI2022] 山河重整
之前打过一场 HA 的 ICPC 萌新赛,了解了这个问题的一个结论:若对于 \(\forall k\in [1,n]\),\(k\) 都能被表示出,那么满足 \(\sum\limits_{j\in S,j\le k}j\ge k\)。这个条件是充要的。并且能被表示出来的数一定会形成一个形如 \([1,x]\) 的区间。
考虑 DP:设 \(f(i,j)\) 表示前 \(i\) 个数选若干个,能表示出 \([1,j]\) 的方案数。\(\mathcal O(n^2),\tt 60pts\)。感谢良心 EI 送这么多分。
然后发现 能表示出 \([1,j]\) 这个条件有点烦,这种 DP 的上限就在 \(\mathcal O(n^2)\) 了,很难优化。
转换视角,既然限制很强,考虑正难则反,容斥一下,算无法表示出 \([1,n]\) 的 \(S\) 个数。
这样有一个相当优秀的性质:对于第一个不能被 \(S\) 表示出的 \(k\),必然有 \(\sum\limits_{j\in S,j\le k}j=k-1\),并且 \(k\) 没有选入 \(S\)。
如果我们可以计算出 \(i\) 是第一个不能被表示出的数的方案数,就能快速解决这个问题。根据推导,这种方式成功的可能性很大。
把限制写出来:\(\sum\limits_{j\in S,j\le i}j=i\),且 \(i+1\) 是 \(S\) 中第一个满足这个条件的数(此处改为 \(i+1\) 是为了方便)。
拆分限制,第二个限制可以用比 \(i\) 小的数容斥去重,具体方法后面再说,我们先考虑第一个限制。
定义 \(f(i)\) 表示 \(1\sim i\) 中选若干个数表示出 \(i\) 的方案数。
\(j\le i\) 的限制是假的,没什么用,这个东西本质就是 \(i\) 的自然数拆分方案,并且不能重复,那么显然至多拆出来 \(\sqrt i\) 个数。可以类比普通的自然数拆分问题,做到 \(\mathcal O(n\sqrt n)\) 计算。具体的计算方法在 这篇题解 里面写的相当清楚,我感觉我也讲不好,就不说了 QAQ。
然后我们考虑容斥掉第二个限制,当我们将 \(j\lt i\) 的 \(f\) 值已经不再有重复,先用 \(f_j\) 进行暴力容斥找找思路:\(f_i\gets f_i-f_j\times A(j,i)\),其中 \(A(j,i)\) 表示容斥系数。
具体地,\(A(j,i)\) 表示 \([j+2,i]\) 中的数凑出 \(i-j\) 的方案数。因为 \(j+2\sim i\) 这部分的空缺需要补上,否则 \(i\) 不满足第一条限制。
考虑计算 \(A(j,i)\),假设选了 \(k(k\ge 1)\) 个数,将所有选中的数减去 \(j+2\),总和减去 \(i\times (j+2)\),又成了原来的问题。
然后会发现,\(j\ge \frac{i}{2}\) 的时候不合法,那么将 \(1\sim i\) 分割为 \(1\sim \frac{i}{2},\frac{i}{2}+1\sim i\),递归求解左半段,计算左半段对右半段的贡献即可。因为右半段自身内部不会产生影响。
类半在线卷积,分治计算即可,过程中用一个辅助数组 \(g\) 处理容斥系数,因为容斥系数也是一个拆分数的形式,所以计算方式和 \(f\) 大同小异。
因为分治只需递归一半,所以复杂度仍为 \(\mathcal O(n\sqrt n)\),使用神奇多项式科技好像可以做到 \(\mathcal O(n^{4/3}\log^{2/3}n)\)。EI 好强。
#include <bits/stdc++.h>
const int maxn = 5e5 + 5;
int n,p,pw[maxn],f[maxn],g[maxn];
void add(int& x,int y) {
x += y;
if(x >= p)
x -= p;
return ;
}
void sub(int& x,int y) {
x -= y;
if(x < 0)
x += p;
return ;
}
void solve(int n) {
if(n <= 1)
return ;
solve(n >> 1);
int s = std::sqrt(n * 2);
for(int i = 0;i <= n;++ i)
g[i] = 0;
for(int i = s;i;-- i) {
for(int j = n;j >= i;-- j)
g[j] = g[j - i];
for(int j = 0;j + (j + 2) * i <= n;++ j)
add(g[j + (j + 2) * i] , f[j]);
for(int j = i;j <= n;++ j)
add(g[j] , g[j - i]);
}
for(int i = (n >> 1) + 1;i <= n;++ i)
sub(f[i] , g[i]);
for(int i = 0;i <= n;++ i)
g[i] = 0;
return ;
}
int main() {
scanf("%d %d",&n,&p);
pw[0] = 1;
for(int i = 1;i <= n;++ i)
pw[i] = pw[i - 1] * 2 % p;
int s = std::sqrt(2 * n);
for(int i = s;i;-- i) {
for(int j = n;j >= i;-- j)
f[j] = f[j - i];
add(f[i] , 1);
for(int j = i;j <= n;++ j)
add(f[j] , f[j - i]);
}
f[0] = 1;
solve(n);
int ans = 0;
for(int i = 0;i < n;++ i)
add(ans , 1ll * f[i] * pw[n - i - 1] % p);
sub(pw[n] , ans);
printf("%d\n",pw[n]);
return 0;
}