[AHOI2022] 山河重整 解题报告
T3,一个不错的数学题,给了不少的暴力分。
Statement
求有多少值域为 \([1,n]\) 的集合,01背包可以凑出 \(1\sim n\) 中的所有数字。
Subtask \(1\sim 6\)
我们从小到大选择每一个数,不难发现凑出来的数字一定是 \([1,n]\) 的一段前缀。
于是考虑 dp,记 \(f_{i,j}\) 表示选择完了前 \(i\) 个数,可以表示出 \([1,j]\) 中的所有数。
如何转移?考虑当前加 \(i + 1\),新扩展的可凑区间为 \([i + 1, i + j + 1]\)。于是,如果要让这段区间造成贡献必须满足 \(j + 1 \ge i + 1 \implies j \ge i\)。
于是转移写成
时间复杂度 \(\mathcal{O}(n^2)\),足以通过 \(n\le 5000\)。
Subtask \(7\sim 10\)
上述 dp 已经不好优化,考虑重新设计状态。
不妨设计容斥,对于一组不合法方案,找到最小的 \(i\),满足 \([1,i]\) 中所有的数都可以凑出,\(i+1\) 无法凑出。
那么就可以转化为,对于一个位置 \(i\),满足 \(i\) 是最小的满足值域在 \([1,i]\) 中恰好凑出元素和为 \(i\)的值。
于是状态变为设 \(f_i\) 表示满足选择的集合值域在 \([1,i]\),能拼出 \([1,i]\),且元素和恰好为 \(i\) 的方案数。
更具体地,我们枚举第一个不能被表示出来的数,在计算 \(f_i\) 时,如果 \(f_j\) 要向 \(f_i\) 转移,需要满足
- \([1,j]\) 均可以被表示
- \(j + 1\) 不能被表示
- \([j+2,i]\) 凑出来的数和为 \(i - j\)
接下来就需要计算 \([1,j]\) 的互异拆分数与 \([j + 2, i]\) 的互异拆分数。
有一个结论:\(\sum a_i=n,len_a \approx \sqrt n\)。
可以将互异拆分数看作一个阶梯。

如图所示,此时 \(a_1 < a_2 < \cdots < a_k\),且 \(\sum_{1}^{k}a_i=n\)
朴素的题目一般给定了拆分部分 \(k\),时间复杂度为 \(\mathcal{O}(nk)\)。
在本题会退化成 \(\mathcal{O}(\sqrt n\cdot n\sqrt n = n ^ 2)\)。
但此时这个做法还是与暴力同分。
考虑如何优化构造,我们将整个图竖过来。

那么现在构造转换成了对于 \(i \in [1, \sqrt{n}]\),求有多少个数大于等于 \(i\)。
在上张图中,不难发现有大于等于 \(i\) 的数有 \(5,4,3,3,2,1\),可以发现 \(i\) 不一定小于 \(\sqrt{n}\),但一定是两倍级别内的。
考虑这样一个数列是递减的,可以从 \(\sqrt{n}\) 到 \(1\) 倒序枚举 \(i\),每次加入一个面积为 \(i\) 的矩形,表示有 \(i\) 个元素大于等于某个值。
对于一个 \(i\),考虑 \(j\) 在贡献的时候其他元素大小应该大于等于 \(j + 2\)。设 \(x\) 为 \([j+2,i]\) 的集合中数的个数时,\(j\) 可以贡献到 \(j + (j + 2) \times x\),因为计算 \([j+2,i]\) 时只要知道 \(f_{i-j}\),且每个值比原来计算出来的少了 \(j + 2\)。
如果我们要让 \(j\) 贡献到 \(i\),只需要满足 \(i\ge j + (j + 2)\)。因此可以借助分治思想,每次处理 \(\frac{i}{2}\)。
复杂度为 \(\mathcal{T}(n) = \mathcal{O}(n\sqrt{n}) + \mathcal{T}(\frac{n}{2}) = \mathcal{O}(n\sqrt{n})\)。
Code
void work(int n) {
if (n <= 1) return;
work(n / 2);
int lim = sqrt(n << 1);
for (int i = lim; i >= 1; --i) {
for (int j = n; j >= i; --j)
g[j] = g[j - i];
for (int j = 0, k = 2 * i; k <= n; ++j, k += i + 1)
add(g[k], f[j]);
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], -g[i] + p);
for (int i = 0; i <= n; ++i) g[i] = 0;
}
int main() {
cin >> n >> p;
pw[0] = 1;
for (int i = 1; i <= n; ++i) pw[i] = (pw[i - 1] << 1) % p;
int lim = sqrt(n << 1);
for (int i = lim; i >= 1; --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;
work(n);
int ans = 0;
for (int i = 0; i < n; ++i) add(ans, 1ll * f[i] * pw[n - i - 1] % p);
cout << (pw[n] - ans + p) % p << endl;
return 0;
}

浙公网安备 33010602011771号