[AHOI2022] 山河重整 题解
做完这个题后基本上把洛谷题解区所有题解都翻了 114514 遍。或许我这水平确实没到做这个题的级别吧(
首先 \(S\) 能凑出 \(1..n\) 的充要条件是对于 \(S=\left\{a_1,a_2..a_k\right\}\),若 \(a\) 已经从小到大排列,则 \(\forall i,\sum\limits^i_{j=1}a_j\ge a_{i+1}\),暴力 dp 一下,60 分白给。
然而暴力 dp 状态数 \(O(n^2)\),并且很难省去状态,所以这条路是走不通的。
尝试统计不合法的方案数,容易发现,不合法的方案形如前面一些数的和为 \(i\),并且 \(i\) 不在集合这种。方案数也就是 \(i\) 的互异拆分数乘上 \(2^{n-i}\)。为方便,记 \(i\) 的互异拆分数为 \(f_i\)。
如果直接减去所有 \(f_i\) 会减多,毕竟不合法方案有可能不合法到离谱以至于有一大堆 \(i\) 满足上述条件。
考虑容斥,令 \(g_i\) 为 \([1,i]\) 中选出一些数,使得 \([1,i-1]\) 所有数都能表示出来,它们的和为 \(i\),\(i\) 未被选中的方案数。
我们有 \(g_i=f_i-\sum\limits_{j=1}^{i-1}g_j\cdot A(j,i)\)。其中 \(A(j,i)\) 表示 \([j+2,i]\) 中选出若干个数,它们的和为 \(i-j\) 的方案数。
我们记 \(h_i\) 使 \(g_i=f_i=h_i\)。
如果知道了到底最终 \([j+2,i]\) 中要选几个数,系数 \(A\) 是容易计算的。但如果直接枚举然后调整求和顺序的话,会发现是根号 log 的复杂度,显然过不去。
我们换一种方式来理解互异拆分数的求法,把每个数看成一列,左边的列长度大,右边的列长度小。这样,从上到下每一行长度依次不升,且相邻两行长度最多相差 \(1\),每种长度的行至少有一个,而第一行的长度,代表我到底选了几个数。行的数量为根号级别。dp 的时候从大到小枚举当前行长度即可。考虑到上面那个 \(\sum\) 意思就是一种和为 \(i-j\) 且所有数都在 \([j+2,i]\) 范围内的方案,会造成 \(g_j\) 的贡献。我们不妨在知道了 \([j+2,i]\) 选 \(k\) 个数后转化成和为 \(i-j-k(j+1)\) ,数的范围任意的方案。注意到,在互异拆分数 dp 的时候,我们当前枚举的 \(k\),当前行长度,也就代表了一种新的第一行长度。那么,在转移到 \(k\) 时,对于每个 \(j\),将 \(h_{j+i(j+2)}\) 加上 \(g_j\) 就跟互异拆分数 dp 一模一样了!(多加一个 \(i\) 代表长度为 \(i\) 的行本身的贡献,意义就是说当前只选了一行长度为 \(i\) 的,但是数字和却有了 \(j+i(j+1)\) 的额外贡献。)
结合网格图理解一下,将 \(i\) 减去 \(j+k(j+2)\) 本质上就是把一个上下都缺一截的形状平移成下面缺一截的形状方便转移,也就不难理解了。
还有一个问题,必须知道了前面的 \(g\) 才能向上面那样转移。然而只有 \(2j+2\le i\) 时 \(g_j\) 才会影响到 \(g_i\),所以我们可以倍增转移。
最终答案:\(2^n-\sum\limits^{n-1}_{i=0}g_i2^{n-i-1}\)。
时间复杂度 \(O(n\sqrt{n})\)。
#include <cstdio>
int f[500005], g[500005], h[500005], pow2[500005], n, mod;
inline void add(int &x, const int y) {
if ((x += y) >= mod) x -= mod;
}
void solve(int n) {
if (n == 1) return;
solve(n >> 1);
int lim = 0;
while (lim * (lim + 1) >> 1 <= n) ++ lim;
-- lim;
for (int i = lim; i; -- i) {
for (int j = n; j >= i; -- j) h[j] = h[j - i];
for (int j = 0; j + i * (j + 2) <= n; ++ j) add(h[j + i * (j + 2)], g[j]);
for (int j = i; j <= n; ++ j) add(h[j], h[j - i]);
}
for (int i = (n >> 1) + 1; i <= n; ++ i) g[i] = (f[i] - h[i] + mod) % mod;
for (int i = 1; i <= n; ++ i) h[i] = 0;
}
int main() {
scanf("%d%d", &n, &mod);
pow2[0] = 1;
for (int i = 1; i <= n; ++ i) pow2[i] = (pow2[i - 1] << 1) % mod;
int lim = 0;
while (lim * (lim + 1) >> 1 <= n) ++ lim;
-- lim;
for (int i = lim; i; -- i) {
for (int j = n; j >= i; -- j) f[j] = f[j - i];
f[i] = 1;
for (int j = i; j <= n; ++ j) add(f[j], f[j - i]);
}
f[0] = g[0] = g[1] = 1;
solve(n);
int ans = 0;
for (int i = 0; i < n; ++ i) add(ans, 1ll * g[i] * pow2[n - i - 1] % mod);
printf("%d", (pow2[n] - ans + mod) % mod);
}