通过判断划分中是否包含 1 分解为两个独立子问题,建立二维 DP / 记忆化搜索状态转移 f(n, k) = f(n-1, k-1) + f(n-k, k)。
OJ: roj
题目 ID: 1675
难度:普及+/提高-
标签:动态规划记忆化搜索组合数学
创建: 2026-03-31 10:00
更新: 2026-10-07 13:50
形式化题目
给定正整数
动态规划 / 记忆化搜索
思路
为了避免排列顺序对计数产生干扰(例如
对于任意一种满足条件的拆分,观察其最小元素
- 最小项为
(即 ): 此时拆分中至少包含一个 。如果拿掉这个 ,剩下 个数的和为 。这等价于将整数 拆分成 份正整数。 - 最小项大于
(即 ): 此时所有分出的数都大于等于 。如果我们给每一份都减去 (共减去 ),则每一份仍为正整数( ),且它们的和变为 。这与将整数 拆分成 份正整数完全一一对应。
上述两类拆分互斥且覆盖了所有合法划分,根据加法原理,设
边界条件:
- 若
或 ,无法拆分成 份正整数,方案数为 ; - 若
或 ,方案唯一(全为 或整除自身),方案数为 。
样例推导表格 ( )
状态转移计算过程如下表所示:
| 1 | 2 | 3 | 转移计算说明 | |
|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 边界 |
| 2 | 1 | 1 | 0 | 边界 |
| 3 | 1 | 1 | 1 | 边界 |
| 4 | 1 | 2 | 1 | |
| 5 | 1 | 2 | 2 | |
| 6 | 1 | 3 | 3 | |
| 7 | 1 | 3 | 4 |
代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-03-31 10:00
# update_at: 2026-03-31 10:00
import sys
from functools import cache
@cache
def count_partitions(remain: int, parts: int) -> int:
"""求将整数 remain 划分为 parts 份正整数的方案数。"""
if remain < parts or parts <= 0:
return 0
if remain == parts or parts == 1:
return 1
# 状态转移:若划分包含 1,对应 count_partitions(remain - 1, parts - 1);
# 若每份均大于 1,每份各减 1,对应 count_partitions(remain - parts, parts)。
return count_partitions(remain - 1, parts - 1) + count_partitions(remain - parts, parts)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
try:
n, k = next(data), next(data)
except StopIteration:
return
ans = count_partitions(n, k)
print(ans)
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 01:38
* update_at: 2026-10-06 01:38
*/
// main.cpp:数的划分,按题解主算法做二维 DP。
// f(n, k) 表示把 n 拆成 k 份正整数的无序方案数:
// 按拆分中最小项是否为 1 分类,可得 f(n, k) = f(n-1, k-1) + f(n-k, k)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 205;
const int MAXK = 10;
ll f[MAXN][MAXK]; // f[n][k]:把 n 拆成 k 份正整数的方案数
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll n, k;
cin >> n >> k;
// 边界:k = 1 时只有一种拆法(n 本身);其余从 2 份开始递推。
for (ll i = 1; i <= n; i++) f[i][1] = 1;
// 递推:f[i][j] = f[i-1][j-1](最小项为 1)+ f[i-j][j](每份减 1 后仍为 j 份正整数)。
for (ll i = 2; i <= n; i++) {
for (ll j = 2; j <= k; j++) {
if (i < j) continue; // 份数比和还大,无法拆分
f[i][j] = f[i - 1][j - 1] + f[i - j][j];
}
}
cout << f[n][k] << endl;
return 0;
}复杂度
- 时间复杂度:状态总数至多
,每个状态的转移仅包含常数次加法,总时间复杂度为 。对于 ,总操作次数在千次以内,运行时间 。 - 空间复杂度:递归栈最大深度为
,记忆化哈希表状态容量为 ,占用空间几兆字节,远低于 限制。
总结
本题是整数划分问题的经典模型。解决“无序划分”计数的突破口在于:
- 有序化思维:按单调不降定序,使集合划分唯一映射到有序序列;
- 两分策略:依据“最小值是否为 1”划分子问题,巧妙规避了枚举具体数值的循环,直接得出简洁优美的
动态规划递推。