题解 P6189 【[NOI Online 入门组]跑步(民间数据)】

· · 题解

本题就是正整数拆分问题,很容易想到一种 O(n^2) 的 DP:

f_{i,j} 表示用了前 i 个数和为 j 的方案数,那么 f_{i,j}=f_{i-1,j}+f_{i,j-i},初始时 f_{0,0}=1

这其实就是完全背包,但只能拿 70 分(也可能乱搞到 80)。

那么怎么办呢?我们采用分块的思路,令 m=\sqrt n ,对于 i<m 的数采用以上完全背包来求,这部分时间复杂度为 O(n\sqrt n)

而对于大于等于 m 的数,采用另外一种 DP:

g_{i,j} 表示用了 i 个大于等于 m 的数和为 j 的方案数,那么 g_{i,j}=g_{i-1,j-m}+g_{i,j-i},初始时 g_{0,0}=1

如果觉得不好理解,可以随便举个例子: 假设当前 $m=1$,你要拆的数是 $11$,其中一种方案是 $11=5+2+2 +2$。初始拆分序列为空,现在有两种操作:操作 $1$ 是在拆分序列中增加一个数 $1$,操作 $2$ 是把当前序列中每个数都增加 $1$ 。那么该方案对应的操作序列为:$12221112$。 容易发现,一种拆分方案和一种操作序列是一一对应的,因此这样 DP 不会重复也不会遗漏。 最后一步,就是把分块的两部分结合一下。枚举第一个部分的总和为 $j$,那第二个部分的总和就为 $n-j$,把两者的方案数乘起来记入答案中,即为 $\sum\limits_{j=0}^n (f_{m-1,j}\times \sum\limits_{i=0}^{m} g_{i,n-j})$。 那么本题总时间复杂度为 $O(n\sqrt n)$ ,代码如下: ```cpp #include <bits/stdc++.h> using namespace std; const int N = 100005; int f[N]; int g[400][N]; int main() { int n, p; cin >> n >> p; int m = sqrt(n) + 1; f[0] = 1; for (int i = 1; i < m; i++) { for (int j = i; j <= n; j++) { f[j] += f[j - i]; f[j] %= p; } } g[0][0] = 1; for (int i = 1; i < m; i++) { for (int j = i; j <= n; j++) { g[i][j] = g[i][j - i]; if (j >= m) g[i][j] += g[i - 1][j - m]; g[i][j] %= p; } } int ans = 0; for (int i = 0; i <= n; i++) { LL sum = 0; for (int j = 0; j < m; j++) sum += g[j][n - i]; sum %= p; ans = (ans + f[i] * sum) % p; } cout << ans << endl; return 0; } ```