记录140
#include<bits/stdc++.h>
using namespace std;
const int N=25; // 定义常量N,表示n的最大范围(题目n<=20)
int n,k; // n表示整数个数,k表示需要选出的个数
int a[N]; // 记录n个整数
int ans=0; // 记录和为素数的组合种类数
// 判断一个数x是否为素数
bool is_prime(int x){
if(x<2) return false; // 小于2的数不是素数
for(int i=2;i*i<=x;i++){
if(x%i==0) return false; // 如果能被i整除,不是素数
}
return true; // 是素数
}
// 深度优先搜索(DFS)
// start: 当前从第几个数开始选(保证组合不重复且有序)
// cur: 当前已经选了几个数
// sum: 当前已选数的总和
void dfs(int start,int cur,int sum){
if(cur==k){ // 如果已经选够了k个数(递归终止条件)
if(is_prime(sum)) ans++; // 判断总和是否为素数,种类数加1
return; // 结束当前递归分支
}
// 可行性剪枝:剩下的数必须足够凑齐k个
// 当前从start开始选,还需要选(k-cur)个数,所以i的上限是 n-(k-cur)+1
// 循环终点 = 总数 - 还需要选的个数 + 1
for(int i=start;i<=n-(k-cur)+1;i++){
dfs(i+1,cur+1,sum+a[i]); // 从下一个位置开始选,已选数量+1,总和加上a[i]
}
}
int main(){ // 主函数入口
ios::sync_with_stdio(false); // 关闭标准流同步,提升输入输出效率
cin.tie(0); // 解除cin与cout的绑定,加快读取速度
cin>>n>>k; // 读入n和k
for(int i=1;i<=n;i++) cin>>a[i];// 读入n个整数
dfs(1,0,0); // 从第1个数开始选,当前选了0个,当前总和为0
cout<<ans; // 输出和为素数的组合种类数
return 0; // 程序正常结束
}
题目传送门
https://www.luogu.com.cn/problem/P1036
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的优快云账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题是一道非常经典的组合枚举与素数判定问题。
-
问题转化:
题目要求从 n个整数中任选 kk 个相加,统计和为素数的组合种类数。这本质上是一个“从 n 个数中取 k 个数的组合问题”(即 Cnk ),而不是排列问题。因为加法满足交换律,3+7+19和7+3+19是同一个组合,我们需要避免重复计算。 -
算法选择(DFS + 剪枝):
由于 n≤20 ,数据规模较小,我们可以使用深度优先搜索(DFS)来暴力枚举所有可能的组合。- 避免重复:在 DFS 过程中,我们通过一个
start参数来记录当前从哪个位置开始选数。每次递归选择下一个数时,都从当前位置的下一个位置(i+1)开始选,从而保证选出的数在原数组中的下标是严格递增的,天然避免了组合的重复。 - 可行性剪枝:在枚举当前层的选择时,如果剩下的数字个数不足以凑齐 kk 个数,就提前停止枚举,减少无效搜索。
- 素数判定:当凑齐 kk 个数时,计算它们的和,并使用试除法判断该和是否为素数。
- 避免重复:在 DFS 过程中,我们通过一个
代码分块详细解释
1. 头文件、常量与全局变量定义
#include<bits/stdc++.h>
using namespace std;
const int N=25; // 定义常量 N,表示 n 的最大范围(题目 n<=20)
int n,k; // n 表示整数个数,k 表示需要选出的个数
int a[N]; // 记录 n 个整数
int ans=0; // 记录和为素数的组合种类数
- 详细分析:这部分定义了算法的基础变量。
a[N]用来存储输入的 nn 个整数,ans作为全局计数器,用来累加满足条件的组合数。
2. 素数判定函数(is_prime)
// 判断一个数 x 是否为素数
bool is_prime(int x){
if(x<2) return false; // 小于 2 的数不是素数
for(int i=2;i*i<=x;i++){
if(x%i==0) return false; // 如果能被 i 整除,不是素数
}
return true; // 是素数
}
- 详细分析:这是一个标准的试除法求素数函数。
- 边界处理:首先排除了小于 2 的情况,因为 0 和 1 不是素数。
- 循环优化:循环的终止条件是
i*i <= x(即 i≤xi≤x )。这是因为如果 xx 有一个大于 xx 的因子,那么它必然有一个小于 xx 的对应因子。这种优化将素数判定的时间复杂度从 O(x)O(x) 降低到了 O(x)O(x) ,对于本题中可能达到 5×106×205×106×20 级别的和来说,效率提升非常明显。
3. 核心逻辑:DFS 组合枚举与剪枝
// 深度优先搜索(DFS)
// start: 当前从第几个数开始选(保证组合不重复且有序)
// cur: 当前已经选了几个数
// sum: 当前已选数的总和
void dfs(int start, int cur, int sum){
if(cur==k){ // 如果已经选够了 k 个数(递归终止条件)
if(is_prime(sum)) ans++; // 判断总和是否为素数,种类数加 1
return; // 结束当前递归分支
}
// 可行性剪枝:剩下的数必须足够凑齐 k 个
// 当前从 start 开始选,还需要选 (k-cur) 个数,所以 i 的上限是 n-(k-cur)+1
// 循环终点 = 总数 - 还需要选的个数 + 1
for(int i=start; i<=n-(k-cur)+1; i++){
dfs(i+1, cur+1, sum+a[i]); // 从下一个位置开始选,已选数量+1,总和加上 a[i]
}
}
- 详细分析:这是代码的灵魂,完美实现了组合枚举。
- 递归终止条件:当
cur == k时,说明已经凑齐了 kk 个数,此时直接调用is_prime(sum)进行判定,如果为真则全局答案ans加 1。 - 避免重复的核心:
for循环的起始值是start,而递归调用时传入的是i+1。这意味着如果当前选择了第i个数,下一层递归只能从i+1及之后的数中选择。这保证了选出的数在原数组中的下标是严格递增的,从而避免了3+7+19和7+3+19这种重复计算。 - 可行性剪枝:循环的终止条件
i <= n-(k-cur)+1是一个经典的组合数学优化。它的意思是:当前还需要选k-cur个数,如果从当前位置i往后数,剩下的数字个数已经不够填满这k-cur个位置了,那么继续枚举i及其后面的数就没有意义了,直接跳出循环。
- 递归终止条件:当
4. 主函数:输入处理与启动搜索
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>k; // 读入 n 和 k
for(int i=1;i<=n;i++) cin>>a[i]; // 读入 n 个整数
dfs(1,0,0); // 从第 1 个数开始选,当前选了 0 个,当前总和为 0
cout<<ans; // 输出和为素数的组合种类数
return 0;
}
- 详细分析:主函数负责数据的读入和算法的启动。
dfs(1,0,0)表示从数组的第 1 个位置开始尝试选择,当前已经选了 0 个数,当前的累加和为 0。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 素数判定 | is_prime(int x) | 试除法判断一个数是否为素数 | 准确判定组合的和是否符合题目要求,i*i<=x 保证了效率 |
| 递归终止 | if(cur==k) | 判断是否已经选够了 k 个数 | 到达搜索树的叶子节点,进行结果统计 |
| 避免重复 | dfs(i+1, ...) | 下一层递归只能从当前数的下一个位置开始选 | 保证选出的组合下标严格递增,避免了排列带来的重复计算 |
| 可行性剪枝 | i <= n-(k-cur)+1 | 限制当前层枚举的上界 | 当剩余数字不足以凑齐 k 个数时提前终止,大幅减少无效搜索 |
| 状态传递 | sum+a[i] | 将当前选择的数累加到总和中 | 在递归过程中动态维护当前组合的总和,避免了在叶子节点重新遍历求和 |
| 全局计数 | ans++ | 累加满足条件的组合数 | 记录最终的答案 |
393



被折叠的 条评论
为什么被折叠?



