0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1025,日期见页头。两边不一致时信原站。
题目描述
将整数 n 分成 k 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。
例如:n = 7,k = 3,下面三种分法被认为是相同的:
1, 1, 5 ;
1, 5, 1 ;
5, 1, 1 。
问有多少种不同的分法。
输入格式:n, k(6 < n ≤ 200,2 ≤ k ≤ 6)。
输出格式:1 个整数,即不同的分法。
说明:样例那四种分法为 1,1,5;1,2,4;1,3,3;2,2,3。
来源:NOIP 2001 提高组第二题。
输入输出样例
输入
7 3
输出
4
n = 7、k = 3 一共有 4 种不同的分法(题面里列出来了)。
上面那段输出是仓库里的 p1025.cpp 真跑出来的。
1先看清楚:难点不在「搜」,在「不重复」
「把 n 分成 k 份」怎么搜谁都会:递归 k 层,每层决定这一份取多少。
⚠ 难的是题面那句「不考虑顺序」—— 它把 1,1,5、1,5,1、5,1,1 算作同一种。
搜出来的是「分法的排列」 <- 天生会重复
题目要的是「分法的集合」 <- 要想办法让每一组数只被数到一次
2第 ① 版:每份都从 1 开始枚举(答案偏大)
// 第 ① 版:每一份都从 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;}点「运行 ▶」看结果
把 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 <- 跳过
// 第 ② 版(推荐写法):每一份都不小于上一份 —— 一句话去掉全部重复//// ★ 关键的一步:**不考虑顺序,就等于「只数不下降的那一种排法」**。// `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;}点「运行 ▶」看结果
★ 这一版就已经能 AC 了(n = 200、k = 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 = 200、k = 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 件」是同一个套路 —— 把方案集合按某个「是 / 否」问题切成两半。
// 第 ③ 版:不搜了,直接递推 —— 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;}点「运行 ▶」看结果
5换一把尺子:三种写法各碰了多少下
// 换一把尺子:三种写法各自「碰了多少下」//// ① 每份从 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 章那张表是同一件事:
「更好的算法」在小数据上常常更贵,它赢在数据变大的时候。