骨牌1

GitHub跳转原题关系图返回列表

把 n 拆成恰好 k 个正整数之和的方案数,用 (剩余和, 剩余段数, 下一段下界) 三元组记忆化搜索。

OJ: roj

题目 ID: 1676

难度:普及-

标签:动态规划记忆化搜索整数划分数学递归

创建: 2026-10-01 02:10

更新: 2026-10-07 13:50

题目描述

给定正整数 nn 和 kk,求把 nn 拆成恰好 kk 个正整数之和的方案数,顺序不同视为同一种。数据范围满足 n≤200n \le 200,k≤7k \le 7 左右。

思路

由于加法可交换,每个方案都可以唯一对应一个非降序列 1≤a1≤a2≤⋯≤ak1 \le a_1 \le a_2 \le \dots \le a_k 且和为 nn。用 ways(rest, parts, low) 表示还要把 rest 分成 parts 段、且下一段最小为 low 的方案数。枚举当前段 head,下界是非降约束 low,上界收紧为 rest / parts(余下每段都至少为 head)。parts == 1 时只要 rest >= low 就唯一方案,parts == 0 时只有 rest == 0 才计 1。用记忆化数组保存三个参数的状态。

参考代码

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-10-01 02:10
# update_at: 2026-10-01 02:10

import sys
from functools import cache


@cache
def ways(rest: int, parts: int, low: int) -> int:
    """把 rest 拆成 parts 个非降正整数、且每段都不小于 low 的方案数。

    非降约定让同一个多重集合只有一种写法,于是"数拆分方案"等同于"数非降序列"。
    """
    if parts == 0:
        return 1 if rest == 0 else 0          # 恰好填满才算一个方案
    if parts == 1:
        return 1 if rest >= low else 0        # 只剩一段时,长度一旦合法就唯一
    # 首段 head 的下界是非降约束 low;上界来自「余下 parts-1 段每段至少 head」
    # 即 head + (parts-1)*head <= rest,等价于 head <= rest // parts。
    heads = range(low, rest // parts + 1)
    return sum(ways(rest - head, parts - 1, head) for head in heads)


def solve() -> None:
    n, k = map(int, sys.stdin.buffer.read().split())
    print(ways(n, k, 1))


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:49
 * update_at: 2026-10-06 01:49
 */
#include <iostream>
#include <cstring>
using namespace std;

typedef long long ll;

ll n, k;

// memo[rest][parts][low] 表示把 rest 拆成 parts 个非降正整数、且每段不小于 low 的方案数。
// n 最大约 200,k 最大约 7,low 最大约 n,用 -1 表示未计算。
ll memo[205][10][205];

// 返回方案数。
ll ways(ll rest, ll parts, ll low) {
    if (parts == 0) return rest == 0 ? 1 : 0;        // 恰好填满才合法
    if (parts == 1) return rest >= low ? 1 : 0;      // 只剩一段,和已固定
    ll &ans = memo[rest][parts][low];
    if (ans != -1) return ans;
    ans = 0;
    // head 的下界是非降约束 low;上界来自余下每段至少 head,即 head * parts <= rest。
    ll up = rest / parts;
    for (ll head = low; head <= up; ++head) {
        ans += ways(rest - head, parts - 1, head);
    }
    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    memset(memo, -1, sizeof(memo));
    cin >> n >> k;
    cout << ways(n, k, 1) << '\n';
    return 0;
}