题意
给定一个数组,里面有n位正整数,要从这个数组里面选取K个数,使得它们的和为S,问有多少种可能的取法;
Input
第一行,一个整数T(T<=100
),指示测试用例的数量。
对于每个情况,有两行。
第一行,三个整数表示n,K和S.其中K<=n<=16
.
第二行n个整数表示n个元素的数组。
数据保证所有数字都可以以 32 位整数存储。
Output
对于每种情况,输出一个整数,代表可能的取法,独占一行。
Sample Input
1
10 3 10
1 2 3 4 5 6 7 8 9 10
Sample Output
4
//1+2+7=10
//1+3+6=10
//1+4+5=10
//2+3+5=10
思路
一般思路为三重循环暴力破解,但是n³复杂度明显过高;
首先,我们可以看出在一个数组中选择组合情况,是很明显的子集枚举问题,我们可以用递归处理。
还有,为了降低复杂度,我们可以在选择的时候做一些合理性剪枝,比如说在小于K个数的时候和已经大于S了,那么就没有必要去继续找了,此趟递归就可以结束了;又或者在递归的时候已经有了K个数,但是其和还小于S,那么就可以结束此趟递归。
过程
在遍历到每一个数a
的时候,我们都有两种选择,就是这个数选或者不选:
如果选了,那么S=S-a;K=K-1;
如果不选,那么K
,S
都不变;
具体过程看以下代码,有详细注释。
代码
#include<iostream>
#include<cstdio>
using namespace std;
int num[17]; //存储数列
int count=0; //满足条件的组合数
void selectNumber(int i,int &n,int x,int &K,int S)
{//递归函数 枚举可能的组合
//i 为数组的索引 n为数组的个数 x为当前选择数的个数 K为总的可以选的个数 S为需要到达的和
if(S==0&&x==K)
{//递归终点 S=0 且 当前的个数x=k
count++;
return ;
}
if(i>=n) return; //遍历终点
if(x>K||S<0) return ; //剪枝 如果当前个数x大于K 总和<0 的时候
selectNumber(i+1,n,x+1,K,S-num[i]);//选
//选择当前i索引点 继续遍历下个点i+1 当前节点个数+1 并且S减去这个当前值
//下一次递归相当于 求 在剩下的数组中 求K-(x+1)个数 使得它们的和为S-num[i]
selectNumber(i+1,n,x,K,S);//不选
//不选当前i索引点 继续遍历下个点 x 不变 S不变
}
int main()
{
int T;
scanf("%d",&T);
int n,K,S;
while(T--)
{
count=0;
scanf("%d %d %d",&n,&K,&S);
for(int i=0;i<n;i++)
{
scanf("%d",&num[i]);
}
selectNumber(0,n,0,K,S);
cout<<count<<endl;
}
return 0;
}
总结
这道题是一道非常基础的递归枚举问题,在多加一些合理的剪枝,注意细节,就可以直接过。