题目描述
小 H 是一个热爱运动的孩子,某天他想给自己制定一个跑步计划。小 H 计划跑n米,其中第分钟要跑
米(
是正整数),但没有确定好总时长。
由于随着跑步时间增加,小 H 会越来越累,所以小 H 的计划必须满足对于任意都满足
。
现在小 H 想知道一共有多少个不同的满足条件的计划,请你帮助他。两个计划不同当且仅当跑步的总时长不同,或者存在一个i,使得两个计划中不相同。
由于最后的答案可能很大,你只需要求出答案对 p 取模的结果。
输入格式
输入只有一行两个整数,代表总米数n和模数 p。
输出格式
输出一行一个整数,代表答案对p取模的结果。
输入输出样例
输入 #1
4 44
输出 #1
5
输入 #2
66 666666
输出 #2
323522
输入 #3
66666 66666666
输出 #3
45183149
说明/提示
样例输入输出 1 解释
五个不同的计划分别是:。
数据规模与约定
本题共 10个测试点,各测试点信息如下表。
测试点编号 | n≤ | 测试点编号 | n≤ |
---|---|---|---|
1 | 55 | 6 | |
2 | 10 | 7 | |
3 | 50 | 8 | |
4 | 100 | 9 | |
5 | 500 | 10 |
对于全部的测试点,保证 ,
。
本题就是正整数拆分问题,很容易想到一种的 DP:
记 ,j 表示用了前 i 个数和为 j的方案数,那么
=
+
,初始时
=1 。
这其实就是完全背包,但只能拿 70分(也可能乱搞到 80分)。
那么怎么办呢?我们采用分块的思路,令 m=,对于 i<m 的数采用以上完全背包来求,这部分时间复杂度为
。
而对于大于等于 m的数,采用另外一种 DP:
记 表示用了i个大于等于m的数和为j的方案数,那么
,初始时
=1 。
- 转移方程的第一项
表示在拆分序列中增加一个数m。
- 转移方程的第二项
表示把当前拆分序列的每个数都加上 11。
i最大为 ,因而这一步时间复杂度为
。
如果觉得不好理解,可以随便举个例子:
假设当前 m=1,你要拆的数是 11,其中一种方案是 11=5+2+2+2。初始拆分序列为空,现在有两种操作:操作 1是在拆分序列中增加一个数1,操作2 是把当前序列中每个数都增加1 。那么该方案对应的操作序列为:122221112。
容易发现,一种拆分方案和一种操作序列是一一对应的,因此这样 DP 不会重复也不会遗漏。
最后一步,就是把分块的两部分结合一下。枚举第一个部分的总和为 jj,那第二个部分的总和就为 n-j,把两者的方案数乘起来记入答案中,即为。
那么本题总时间复杂度为 ,代码如下:
#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;
}