题单 · 习题解析

洛谷 P1025 数的划分

「不考虑顺序」= 只数不下降的那一种排法;⚠ 有重复元素时不能靠「除以 k!」去重

原题:洛谷 P1025出自 第 4 章 回溯与状态恢复:N 皇后 的题单题面本地存档:2026-08-26
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1025,日期见页头。两边不一致时信原站。

题目描述

将整数 n 分成 k 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。

例如:n = 7k = 3,下面三种分法被认为是相同的

1, 1, 5 ;
1, 5, 1 ;
5, 1, 1 。

问有多少种不同的分法。

输入格式n, k6 < n ≤ 2002 ≤ k ≤ 6)。

输出格式:1 个整数,即不同的分法。

说明:样例那四种分法为 1,1,51,2,41,3,32,2,3

来源:NOIP 2001 提高组第二题。

输入输出样例

输入

7 3

输出

4

n = 7k = 3 一共有 4 种不同的分法(题面里列出来了)。 上面那段输出是仓库里的 p1025.cpp 真跑出来的。

1先看清楚:难点不在「搜」,在「不重复」

「把 n 分成 k 份」怎么搜谁都会:递归 k 层,每层决定这一份取多少。 ⚠ 难的是题面那句「不考虑顺序」—— 它把 1,1,51,5,15,1,1 算作同一种

搜出来的是「分法的排列」                     <- 天生会重复
题目要的是「分法的集合」                     <- 要想办法让每一组数只被数到一次

2第 ① 版:每份都从 1 开始枚举(答案偏大)

p1025Order.cpp第 ① 版:数的是「有顺序的分法」
样例应该输出 4,它给 15。
// 第 ① 版:每一份都从 1 开始枚举 —— 它数的是「有顺序的分法」
//
// 最直白的翻译:递归 k 层,第 j 层决定第 j 份分多少,最后一份用掉剩下的全部。
// ⚠ 而它把 `1,1,5`、`1,5,1`、`5,1,1` 当成三种分法 —— 题目明说这三种**是同一种**。
//
// ★ 这一处**样例就能挡住**:n = 7、k = 3 的正确答案是 4,它给 15。
//
// 输入:一行两个整数 n 和 k。
#include <bits/stdc++.h>
using namespace std;
int n, k;
long long ans = 0, calls = 0;
/** 已经分了 cnt 份,还剩 rest 要分完 */
void dfs(int cnt, int rest) {
calls++;
if (cnt == k - 1) { // 只剩最后一份了:它就是剩下的全部
if (rest >= 1) ans++;
return;
}
for (int x = 1; rest - x >= k - cnt - 1; x++) // ⚠ 从 1 开始,没有「不小于上一份」这个限制
dfs(cnt + 1, rest - x);
}
int main(int argc, char** argv) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
dfs(0, n);
cout << ans << '\n';
if (argc > 1 && string(argv[1]) == "calls") cerr << "dfs 调用了 " << calls << " 次\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 15 是怎么来的:它数的是「有顺序」的分法

把 7 分成 3 个正整数、认顺序,答案正好是 15(组合数学里叫「隔板法」: C(6, 2) = 15)。而题目要的是不认顺序的那 4 种。

★ 这个病和 P1036 选数P1157 组合的输出同一个: 排列树天生会把同一组数数很多遍。 ⚠ 但这道题不能像它们那样除以 k! —— 1,1,5 里有两个 1, 它的排列只有 3 种而不是 3! = 6 种。有重复元素时,倍数不是常数。

⇒ 所以这道题只能从根上解决:让每一组数只被搜到一次。

3★ 关键的一步:只数「不下降」的那一种排法

一组数的所有排法里,「不下降」的那一种有且只有一个。 ⇒ 规定「这一份 ≥ 上一份」,每组数就恰好被数到一次。

   1,1,5     <- 不下降 ✓ 数它
   1,5,1     <- 跳过
   5,1,1     <- 跳过
p1025.cpp第 ② 版:每份不小于上一份(能 AC)
// 第 ② 版(推荐写法):每一份都不小于上一份 —— 一句话去掉全部重复
//
// ★ 关键的一步:**不考虑顺序,就等于「只数不下降的那一种排法」**。
// `1,1,5` / `1,5,1` / `5,1,1` 里只有 `1,1,5` 是不下降的,
// 所以只要规定「这一份 ≥ 上一份」,每组数就恰好被数到一次。
// (和 [P1157 组合的输出](/sol/p1157) 是同一件事:排列 → 组合,只改 for 的起点。)
//
// ⚠ 循环的上界那一句也要想清楚:这一份取 x 之后还剩 rest − x、还要分 k − cnt − 1 份,
// 而每一份都不小于 x ⇒ 只有 (k − cnt − 1) × x ≤ rest − x 时才走得下去。
// ★ 这一条剪枝不是「优化」,它是**正确性的一部分**在效率上的顺带好处 ——
// 没有它,n = 200、k = 6 要多跑 20 多倍(正文第 ④ 步有实测)。
#include <bits/stdc++.h>
using namespace std;
int n, k;
long long ans = 0, calls = 0;
/** 已经分了 cnt 份,上一份是 last,还剩 rest */
void dfs(int cnt, int last, int rest) {
calls++;
if (cnt == k - 1) { // 只剩最后一份了:它就是剩下的全部
if (rest >= last) ans++; // ⚠ 它也要 ≥ 上一份,否则这组数根本不是不下降的
return;
}
for (int x = last; (long long)(k - cnt - 1) * x <= rest - x; x++)
dfs(cnt + 1, x, rest - x);
}
int main(int argc, char** argv) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
dfs(0, 1, n);
cout << ans << '\n';
if (argc > 1 && string(argv[1]) == "calls") cerr << "dfs 调用了 " << calls << " 次\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这一版就已经能 AC 了n = 200k = 6 本机 0.02 秒)。

★ 循环上界那一句,是「正确性」顺带送的一笔效率
for (int x = last; (long long)(k - cnt - 1) * x <= rest - x; x++)

它说的是:这一份取 x 之后还剩 rest − x、还要分 k − cnt − 1 份, 而每一份都不小于 x —— 所以 x 大到一定程度就再也分不下去了,不用往下试。

⚠ 这不是「优化」,它是「不下降」这条规矩的直接后果。 而它顺带把搜索砍掉了一大截:n = 200k = 6 时, 第 ① 版要 25.4 亿次调用(5.39 秒),第 ② 版只要 434 万次(0.02 秒)—— 差 584 倍。

4第 ③ 版:其实不用搜,两行递推就够

★ 设 f[i][j] = 把 i 分成 j 份有几种。按「有没有哪份是 1」分家,不重不漏:

   至少有一份是 1   -> 把那个 1 拿掉      -> f[i-1][j-1]
   每一份都 ≥ 2     -> 每份都减 1         -> f[i-j][j]

   f[i][j] = f[i-1][j-1] + f[i-j][j]

⚠ 两类为什么不会重:第一类里至少有一个 1,第二类里一个 1 都没有。 ★ 这种拆法和第 24 章背包「选不选第 i 件」是同一个套路 —— 把方案集合按某个「是 / 否」问题切成两半

p1025Dp.cpp第 ③ 版:递推,只填 n × k 个格子
// 第 ③ 版:不搜了,直接递推 —— O(nk) 个格子
//
// ★ 设 f[i][j] = 把 i 分成 j 份(每份 ≥ 1,不计顺序)有几种。分两类,不重不漏:
//
// · 至少有一份是 1 ⇒ 把那个 1 拿掉,剩下的是「i−1 分成 j−1 份」 ⇒ f[i-1][j-1]
// · 每一份都 ≥ 2 ⇒ 每份都减 1,变成「i−j 分成 j 份」 ⇒ f[i-j][j]
//
// f[i][j] = f[i-1][j-1] + f[i-j][j]
//
// ⚠ 第二类为什么不会和第一类重:第一类里**至少有一个 1**,第二类里**一个 1 都没有**。
// ★ 这种「按有没有 1 分家」的拆法,和第 24 章背包「选不选第 i 件」是同一个套路。
//
// 边界:f[0][0] = 1(什么都不分,算一种);i < j 时是 0(每份至少 1,分不出那么多份)。
#include <bits/stdc++.h>
using namespace std;
long long f[205][10];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
f[0][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= k; j++) {
f[i][j] = f[i - 1][j - 1];
if (i >= j) f[i][j] += f[i - j][j];
}
cout << f[n][k] << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

5换一把尺子:三种写法各碰了多少下

p1025Count.cpp数「dfs 调用了多少次」
① 和 ② 数的是 dfs 调用次数;③ 数的是填了多少个格子(固定 n × k)。
// 换一把尺子:三种写法各自「碰了多少下」
//
// ① 每份从 1 起枚举(数的是有顺序的分法)—— dfs 调用次数
// ② 每份不小于上一份 —— dfs 调用次数
// ③ 递推 —— 填了多少个格子(固定 n × k)
//
// ★ 正文第 ④ 步那张表就是它打的。用法:./p1025Count <n> <k> [csv]
#include <bits/stdc++.h>
using namespace std;
int n, k;
long long callsA = 0, ansA = 0, callsB = 0, ansB = 0;
void dfsA(int cnt, int rest) {
callsA++;
if (cnt == k - 1) { if (rest >= 1) ansA++; return; }
for (int x = 1; rest - x >= k - cnt - 1; x++) dfsA(cnt + 1, rest - x);
}
void dfsB(int cnt, int last, int rest) {
callsB++;
if (cnt == k - 1) { if (rest >= last) ansB++; return; }
for (int x = last; (long long)(k - cnt - 1) * x <= rest - x; x++) dfsB(cnt + 1, x, rest - x);
}
int main(int argc, char** argv) {
n = (argc > 1) ? atoi(argv[1]) : 7;
k = (argc > 2) ? atoi(argv[2]) : 3;
bool csv = (argc > 3 && string(argv[3]) == "csv");
dfsA(0, n);
dfsB(0, 1, n);
long long cells = (long long)n * k;
if (csv) {
cout << "ordered," << ansA << '\n';
cout << "answer," << ansB << '\n';
cout << "callsA," << callsA << '\n';
cout << "callsB," << callsB << '\n';
cout << "cells," << cells << '\n';
} else {
cout << "n = " << n << ",k = " << k << '\n';
cout << "① 每份从 1 起(有顺序) 答案 " << ansA << ",dfs 调用 " << callsA << " 次\n";
cout << "② 每份不小于上一份 答案 " << ansB << ",dfs 调用 " << callsB << " 次\n";
cout << "③ 递推 答案同上,只填了 " << cells << " 个格子\n";
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

本机实测(B 机:原生 Ubuntu / i5-10210U 8 线程 / 18 GB,2026-08-26,独占):

n, k ① 有顺序(错的) ② 不下降 ③ 递推
7, 3 21 次 7 次 21 格
40, 6 658 008 次 2 485 次 240 格
100, 6 75 287 520 次 158 130 次 600 格
200, 6 2 535 650 040 次(5.39 秒) 4 342 521 次(0.02 秒) 1 200 格(0.00 秒)

★ 注意最上面那一行:n = 7 时递推反而「碰得最多」(21 格 vs 7 次调用)—— 递推的代价是 n × k 这个固定的量,和答案有多大无关。 ⚠ 这和第 52 章那张表是同一件事: 「更好的算法」在小数据上常常更贵,它赢在数据变大的时候。

这一页记住三句话
  1. 「不考虑顺序」= 只数不下降的那一种排法 —— 一句 x ≥ 上一份 就够了。
  2. 有重复元素时,不能靠「除以 k!」去重 —— 1,1,5 只有 3 种排法,不是 6 种。 (这一点和 P1036 / P1157 不一样,别顺手套。)
  3. ★★ 三条路都对,代价的形状完全不同:搜索的代价跟着答案走, 递推的代价是 n × k 这个死数。所以 n = 7 时搜索更省,n = 200 时递推快到量不出来。