[NOIP 2002 普及组] 选数
用递增下标的 DFS 组合枚举选出 k 个数,只生成 C(n,k) 个组合,组合和用试除法判素数。
OJ: luogu
题目 ID: P1036
难度:普及-
标签:枚举组合素数DFS
日期: 2026-07-15 21:30
形式化题目
给定集合
思路
先看一个可以直接验证想法的朴素解:
/**
* 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-08-13 13:19
* update_at: 2026-08-13 13:21
*/
// brute.cpp:小数据暴力解,使用 01 序列 / 选择序列递归枚举所有可能。
/* 每层递归决定第 i 个数选/不选,叶子节点统一检查选了 k 个且和为素数。 */
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n, k;
int a[MAXN]; // 输入数组
int choose[MAXN]; // choose[i] = 1 表示选了第 i 个数
int ans;
// 判断 x 是否为素数:只需试除到 sqrt(x)
bool is_prime(int x) {
if (x < 2) return false;
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
// 检查当前完整 choose[1..n]:选了恰好 k 个且和为素数
void check() {
int cnt = 0, sum = 0;
for (int i = 1; i <= n; i++) {
if (choose[i]) {
cnt++;
sum += a[i];
}
}
if (cnt == k && is_prime(sum)) ans++;
}
// dfs(dep):正在决定第 dep 个数的选择(0 不选,1 选)
void dfs(int dep) {
if (dep == n + 1) {
check(); // 一条完整 01 序列生成完毕,在叶子节点统一检查
return;
}
// 这一层枚举第 dep 个数的 01 选择
for (int x = 0; x <= 1; x++) {
choose[dep] = x;
dfs(dep + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
dfs(1); // 从第 1 个数开始做选择
cout << ans << "\n";
return 0;
}这个暴力把问题看成一串 01 选择:choose[i] = 0/1 表示第 i 个数不选或选。递归先生成完整的 choose[],到叶子节点再检查是否恰好选了
关键观察是组合不关心顺序,下标递增可以天然去重:规定“每次新选的下标必须大于上一次选的下标”,那么每个
于是主解直接用组合枚举 DFS:dfs(dep, last, sum) 表示正在选第 dep 个数、上一个选中下标是 last、当前和为 sum;每层枚举下标 last + 1 到 sum 是否为素数并计数。这正好把全部
样例搜索树
这张图展示样例
dfs(1, 0, 0) 上界 i ≤ n-(k-1) = 2
├─ i=1 (选 3) → dfs(2, 1, 3) 上界 i ≤ 3
│ ├─ i=2 (选 7) → dfs(3, 2, 10) 上界 i ≤ 4
│ │ ├─ i=3 (选 12) → 组合和 22 合数
│ │ └─ i=4 (选 19) → 组合和 29 素数 ✓
│ └─ i=3 (选 12) → dfs(3, 3, 15) 上界 i ≤ 4
│ └─ i=4 (选 19) → 组合和 34 合数
└─ i=2 (选 7) → dfs(2, 2, 7) 上界 i ≤ 3
└─ i=3 (选 12) → dfs(3, 3, 19) 上界 i ≤ 4
└─ i=4 (选 19) → 组合和 38 合数观察要点:每条路径的下标都严格递增,所以
代码
/**
* 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-08-13 13:19
* update_at: 2026-08-13 13:21
*/
/* P1036 [NOIP 2002 普及组] 选数 */
/* 递增下标 DFS 枚举所有选 k 个数的组合,求和并判断是否为素数。 */
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n, k;
int a[MAXN]; // 输入数组
int ans; // 和为素数的组合个数
// 判断 x 是否为素数:只需试除到 sqrt(x)
bool is_prime(int x) {
if (x < 2) return false;
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
// dfs(dep, last, sum):正在选第 dep 个数(从 1 开始),
// 上一个选中的下标是 last,当前已选数字的和是 sum
void dfs(int dep, int last, int sum) {
if (dep == k + 1) {
// 已经选满 k 个数,判断和是否为素数
if (is_prime(sum)) ans++;
return;
}
// 下标递增去重:第 dep 个数只能从 last+1 往后选。
// 上界剪枝:之后还要选 k - dep 个数,下标 i 最多到 n - (k - dep)
for (int i = last + 1; i <= n - (k - dep); i++) {
dfs(dep + 1, i, sum + a[i]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
dfs(1, 0, 0); // 正在选第 1 个数,上一个下标是 0,当前和为 0
cout << ans << "\n";
return 0;
}Guide 风格代码
cppbook《C++ 快速入门》教学风格的写法(std:: 前缀、i += 1 循环、0 起始下标):
/**
* 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-08-14 14:55
* update_at: 2026-08-14 14:55
*/
#include <iostream>
const int max_n = 25;
int n, k;
int a[max_n]; // 输入的数字,下标从 1 开始
int chosen[max_n]; // chosen[i] = 1 表示第 i 个数已被选中
int answer = 0;
// x 是素数当且仅当 2..sqrt(x) 都除不尽它
bool is_prime(int x) {
if (x < 2) {
return false;
}
for (int i = 2; i * i <= x; i += 1) {
if (x % i == 0) {
return false;
}
}
return true;
}
// dep: 已经选了几个数;last: 上一个选中的下标;sum: 已选数字的和
void pick(int dep, int last, int sum) {
if (dep == k) {
// 选满 k 个数,判断和是否为素数
if (is_prime(sum)) {
answer += 1;
}
return;
}
// 新选的下标必须比 last 大,每个组合只会出现一次;
// 上界 n - (k - dep) + 1 保证后面还能选满 k - dep 个数
for (int i = last + 1; i <= n - (k - dep) + 1; i += 1) {
chosen[i] = 1;
pick(dep + 1, i, sum + a[i]);
chosen[i] = 0; // 回溯恢复,别的组合才能复用这个下标
}
}
int main() {
std::cin >> n >> k;
for (int i = 1; i <= n; i += 1) {
std::cin >> a[i];
}
pick(0, 0, 0);
std::cout << answer << '\n';
return 0;
}复杂度
- 时间:组合枚举生成
个组合,每个组合做一次 的试除判断( , ),总 ;实际试除遇到小因子会提前退出,常数很小。 - 空间:
,递归栈深度至多 层。
总结
本题是“选 combination),本解即由该模板改造而来。
图示解析
这张 ASCII 图展示整道题的解题路线:
朴素想法(brute.cpp)
01 序列枚举:choose[i] = 0/1,生成全部 2^n 个子集
叶子统一检查:恰好选 k 个 且 和为素数
|
| 瓶颈:枚举对象是 2^n 个子集,多数不是 k 元集合
v
关键观察
组合不关心顺序;下标递增 ⇒ 每个 k 元集合只出现一次
选满 k 个还需 k-dep 个数 ⇒ 循环上界 n-(k-dep)
|
v
组合枚举 DFS(main.cpp)
dfs(dep, last, sum):正在选第 dep 个数
i 从 last+1 枚举到 n-(k-dep),不重不漏且必能选满
选满 k 个 → is_prime(sum) 试除法计数
|
v
复杂度 O(C(n,k) * sqrt(S)),空间 O(n+k)图中三条主线分别对应“暴力在哪里慢”“观察到什么性质”“正式解如何利用这个性质”。组合枚举的核心是把“恰好选 last 的下标,且上界保证之后一定能选满,枚举数量从