[AHOI2022] 山河重整
[AHOI2022] 山河重整
考虑集合 \(S \subseteq \{1,2,\dots,n\}\) 合法的充要条件。
根据构造合法集合 \(S\) 的过程,不难给出如下断言:
- 对于 \(i \in [1,n]\),满足 \(\sum\limits_{x \in S \land x \leq i} x \geq i\)
考虑容斥原理,我们假设不满足条件的 \(i\) 中最小值为 \(i_0\),并在 \(i_0\) 处统计。
考虑这意味着什么。首先 \(i_0\) 不能被选中,否则 \(i_0\) 就会满足条件。
其次既然 \(i_0\) 已经不合法了,此时 \(> i_0\) 的数是可以任意选择的。
考虑 \(\leq i_0 - 1\) 的数合法,意味着 \(S\) 内 \(\leq i_0 - 1\) 的数和 \(\geq i_0 - 1\)。
然而 \(i_0\) 不能被选且 \(\leq i_0\) 的数和 \(< i_0\),故得知以下结论:
- \(\sum\limits_{x \in S \land x \leq i_0 - 1} x = i_0 - 1\)
我们记 \(f_i\) 表示满足以下两条性质的集合 \(S \in \{1,2,\dots,n\}\) 个数:
-
\(\sum\limits_{x \in S} x = i\)
-
对于 \(j \in [1,i]\),满足 \(\sum\limits_{x \in S \land x \leq j} x \geq j\)
这样的问题仍然较难计算,考虑再次容斥。
我们记 \(g_i\) 表示满足以下性质的集合 \(S \subseteq \{1,2,\dots,n\}\) 个数:
- \(\sum\limits_{x \in S} x = i\)
考虑 \(g_i\) 如何计算,这类似于完全背包。
具体地,我们声称 \(|S| \leq O(\sqrt{n})\),容易证明这是显然的。
接下来画出类似柱状图的东西,比如选择了 \(\{1,3,4,6\}\) 就画出下图:

容易发现我们可以将柱状图横过来看。
显然此时的柱状图高度 \(h\) 一定满足 \(1 + 2 + \dots + h \leq n\),先找到最大的 \(h\)。
我们有 \(h = O(\sqrt{n})\),并且 \(1\) 到 \(h\) 每个数至少被加一次。
显然这可以用类似完全背包的形式转移,使用滚动数组优化空间到 \(O(n)\)。
显然,横向和纵向的方案形成了对应关系。
每次转移后,我们尝试更新答案,复杂度是 \(O(n \sqrt{n})\)。
接下来考虑用 \(g_i\) 算出 \(f_i\),仍然考虑容斥。
我们枚举最小的 \(j < i\) 使得 \(\sum\limits_{x \in S \land x \leq j} x < j\)。
显然 \(\sum\limits_{x \in S \land x \leq j-1} x = j-1\),且此时 \(j\) 必然不选。
同时有 \(\sum\limits_{x \in S \land j+1 \leq x \leq i} x = i - j + 1\)。
考虑前者的贡献,显然为 \(f_{j-1}\)。
接下来我们考虑后者的贡献,一个显然的事实是 \(i - j + 1 > 0\)。
也就是说我们至少会选择一个数,故左侧式 \(\geq j+1\)。
所以 \(i - j + 1 \geq j + 1\),得 \(2j \leq i\)。
考虑先求出 \(\lfloor \dfrac{n}{2} \rfloor\) 以内的所有 \(f_i\)。
接下来 \(\lfloor \dfrac{n}{2} \rfloor + 1\) 以上的 \(f_i\) 只可能由前面的部分转移。
考虑枚举拆分出数的个数,记作 \(k\)。
我们将这 \(k\) 个数每个减去 \(j\),最后的和变为 \(i-(k+1)j+1\),乘上对应的 \(f\) 即可。
时间复杂度 \(O(n \sqrt{n})\)。
代码一股 AI 味是因为让 AI 卡常。
#include<iostream>
#include<cstdio>
using namespace std;
long long mod;
long long f[500010], g[500010], dp[500010], pow_2[500010];
int max_cnt[500010];
void calc(int n) {
if (n == 1) {
f[0] = f[1] = 1;
return;
}
const long long MOD = mod;
int new_n = n >> 1; // 用位运算代替除法
calc(new_n);
// 复制数组
for (int i = new_n + 1; i <= n; ++i) {
f[i] = g[i];
}
// 清零
for (int i = 0; i <= n; ++i) {
dp[i] = 0;
}
int max_i = max_cnt[n];
for (int i = max_i; i >= 1; --i) {
int step = i;
// 移位
for (int j = n; j >= step; --j) {
dp[j] = dp[j - step];
}
// 内层循环优化
int limit = n;
for (int j = 1; (step + 1) * j + step - 1 <= limit; ++j) {
int idx = (step + 1) * j + step - 1;
dp[idx] += f[j - 1];
if (dp[idx] >= MOD) dp[idx] -= MOD;
}
// 累加
for (int j = step; j <= n; ++j) {
dp[j] += dp[j - step];
if (dp[j] >= MOD) dp[j] -= MOD;
}
}
for (int i = new_n + 1; i <= n; ++i) {
f[i] -= dp[i];
if (f[i] < 0) f[i] += MOD;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n >> mod;
const long long MOD = mod;
int pre_num = 0, pre_sum = 0;
pow_2[0] = 1;
for (int i = 1; i <= n; ++i) {
pow_2[i] = (pow_2[i - 1] << 1) % MOD; // 用移位代替乘法
while (pre_sum + pre_num + 1 <= i) {
++pre_num;
pre_sum += pre_num;
}
max_cnt[i] = pre_num;
}
// 初始化 dp 和 g
for (int i = 0; i <= n; ++i) dp[i] = 0;
int max_i = max_cnt[n];
for (int i = max_i; i >= 1; --i) {
int step = i;
for (int j = n; j >= step; --j) {
dp[j] = dp[j - step];
}
dp[step] = (dp[step] + 1) % MOD;
for (int j = step; j <= n; ++j) {
dp[j] += dp[j - step];
if (dp[j] >= MOD) dp[j] -= MOD;
}
}
dp[0] = 1;
for (int i = 0; i <= n; ++i) {
g[i] = dp[i];
}
calc(n);
long long ans = pow_2[n];
for (int i = 1; i <= n; ++i) {
ans -= f[i - 1] * pow_2[n - i] % MOD;
if (ans < 0) ans += MOD;
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号