洛谷之选数:不要以为暴力遍历就不能用递归写出来!

689 阅读2分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

一道看似暴力实则要动脑筋的题

题目描述

已知 nn 个整数 x1,x2,,xnx_1,x_2,\cdots,x_n,以及 11 个整数 kk<nk(k<n)。从 nn 个整数中任选 kk 个整数相加,可分别得到一系列的和。例如当 n=4k=34n=4,k=3,4 个整数分别为 3,7,12,193,7,12,19 时,可得全部的组合与它们的和为:

3+7+12=223+7+19=297+12+19=383+12+19=343+7+12=22\\ 3+7+19=29\\ 7+12+19=38\\ 3+12+19=34\\

现在,要求你计算出和为素数共有多少种。

例如上例,只有一种的和为素数:3+7+19=293+7+19=29.

输入格式

第一行两个空格隔开的整数 n,k1n20k<nn,k(1≤n≤20,k<n)

第二行 nn 个整数,分别为 x1,x2,,xn1xi5×106x_1,x_2,\cdots,x_n(1 \le x_i \le 5\times 10^6)

输出格式

输出一个整数,表示种类数。

输入输出样例

输入

4 3
3 7 12 19

输出

1

思路:真的有看上去这么简单吗?

大家第一眼看到这个题,就会发现,对对于题目的样例,直接来3个for 循环暴力遍历不就可以了吗???

然而,这其中的k是变化的。也就是说,k=4时,你要搞4个for循环,k=20时你要搞20个for循环...

不瞒你说还真有人这样做了:

if (k==2)  
     {
      for (int i=1;i<=n-1;i++) 
       for (int i1=i+1;i1<=n;i1++) 
        if (zs(a[i]+a[i1])) s++;//计算
        return;
     }
   ...
    if (k==5)  
     {
       for (int i=1;i<=n-4;i++) 
        for (int i1=i+1;i1<=n-3;i1++) 
         for (int i2=i1+1;i2<=n-2;i2++) 
          for (int i3=i2+1;i3<=n-1;i3++)
           for (int i4=i3+1;i4<=n;i4++)
          if(zs(a[i]+a[i1]+a[i2]+a[i3]+a[i4]))s++;
          return;
     }
   ...

看到这种代码我只能说,凑代码行数算是给你整明白了(当然也没有嘲讽写这种代码的人的意思)

不用for循环解决遍历问题?

让我们祭出一个大杀器:递归

我们观察一下遍历的过程:

kk个下标i1,...,iki_1,...,i_k来表示选中的数字

如何遍历i1,...,iki_1,...,i_k?通常的做法是:

固定i1,...,iki_1,...,i_k的顺序,让 i1i_1iki_k 始终从左至右。 那么遍历的方法是,把 i1i_1 固定在所有可以占据的位置上,然后 i2,...,iki_2,...,i_ki1i_1 右边的区间递归的遍历。

比如一个区间 [1,20][1,20] , 如何遍历 i1,...,i10i_1,...,i_10?那就是先令 i1i_1 为1,然后让问题等价于 在区间 [2,20][2,20] , 如何遍历 i2,...,i10i_2,...,i_10;再令 i1i_122,然后让问题等价于 在区间 [3,20][3,20] , 如何遍历 i2,...,i10i_2,...,i_10...知道 i1i_11111 时再在区间 [12,20][12,20] , 遍历 i2,...,i10i_2,...,i_10,那么整个遍历结果就出来了。

这样想的话,我们就能够通过很多子问题来解决父问题了,这就意味着我们可以写递归函数了!

void recur(int i,int remain,int now){
	if(remain==1){
		for(;i<=n-remain;i++){
			//if(!isnot[arr[i]+now])cnt++;
			if(prime(arr[i]+now))cnt++;
		}
		return;
	}
	for(;i<=n-remain;i++){
		recur(i+1,remain-1,now+arr[i]);
	}
	return;
}

其中i是遍历区间的起始点(重点是n-1),remain是还有多少个下标需要遍历,now是前面的数字固定时,遍历到这个子问题时已经确定的加和。

全部代码

#include<iostream>
#include<cmath>
using namespace std;
const int maxn=(int)1e8;
int arr[30];
int isnot[maxn];
int cnt;
int n,k;
int prime(int n){
	if(n==2)return 1;
	for(int i=2;i<=sqrt(n)+1;i++){
		if(n%i==0)return 0;
	}
	return 1;
}
void recur(int i,int remain,int now){
	if(remain==1){
		for(;i<=n-remain;i++){
			//if(!isnot[arr[i]+now])cnt++;
			if(prime(arr[i]+now))cnt++;
		}
		return;
	}
	for(;i<=n-remain;i++){
		recur(i+1,remain-1,now+arr[i]);
	}
	return;
	
}

int main(){
	
	cin>>n>>k;
	for(int i=0;i<n;i++){
		cin>>arr[i];
	}
	recur(0,k,0);
	cout<<cnt;
}