洛谷P1036 选数 题解 简单搜索/简单状态压缩枚举
题目链接:https://www.luogu.com.cn/problem/P1036
题目描述
已知 \(n\) 个整数 \(x_1,x_2,…,x_n\) ,以及 \(1\) 个整数 \(k(k<n)\) 。
从 \(n\) 个整数中任选 \(k\) 个整数相加,可分别得到一系列的和。
例如当 \(n=4,k=3\) , \(4\) 个整数分别为 \(3,7,12,19\) 时,可得全部的组合与它们的和为:
\(3+7+12=22\)
\(3+7+19=29\)
\(7+12+19=38\)
\(3+12+19=34\) 。
现在,要求你计算出和为素数共有多少种。
例如上例,只有一种的和为素数:\(3+7+19=29\) 。
输入格式
键盘输入,格式为:
\(n,k(1 \le n \le 20,k<n)\)
\(x_1,x_2,…,x_n (1 \le x_i \le 5000000)\)
输出格式
屏幕输出,格式为: \(1\)个整数(满足条件的种数)。
样例输入1
4 3
3 7 12 19
样例输出1
1
题解
搜索解法
首先,我们可以把这个问题拆分成两个子问题:
- 从 \(n\) 个数中选出 \(k\) 个数;
- 判断选出的 \(k\) 个数之和是不是素数。
其中第2个子问题很好处理,我们只需要开一个 check(int num)
函数判断 num
是不是素数即可。
判断素数 代码段:
bool check(int num) {
if (num < 2) return false; // 小于2的数不是素数
for (int i = 2; i * i <= num; i ++) { // 从2到根号n遍历i
if (num % i == 0) // 如果i能够整除num,说明num不是素数
return false;
}
return true;
}
然后我们再回过头来分析第1个问题——从 \(n\) 个数中选出 \(k\) 个数。
我们假设给我们的 \(n\) 个数是 \(a[0],a[1],a[2], \dots , a[n-1]\) ,则:
我们可以开函数 dfs(int id, int cnt, int sum)
来表示:
- 我当前放到了第 \(id\) 给位置(所以我现在正在思考要不要选择 \(a[id]\));
- 我已经选择了 \(cnt\) 个元素;
- 我已经选择了的这 \(cnt\) 个元素之和是 \(sum\) 。
所以,如果当前已经满足 \(cnt=k\) ,则说明我已经选择了 \(k\) 个元素,此时只需要判断 \(sum\) 是不是素数(是素数则答案\(+1\))并return。
否则,如果当前 \(id \ge n\) ,说明前面 \(n\) 个元素都选了也没有选够 \(k\) 个元素,则直接return。
否则(\(id \lt n\) 且 \(cnt \lt k\)),则我当前面临两个选择:
- 选择第 \(id\) 个元素,此时进行
dfs(id+1, cnt+1, sum+a[id])
; - 不选择第 \(id\) 个元素,此时进行
dfs(id+1, cnt, sum)
。
实现代码如下:
#include <bits/stdc++.h>
using namespace std;
bool check(int num) {
if (num < 2) return false; // 小于2的数不是素数
for (int i = 2; i * i <= num; i ++) { // 从2到根号n遍历i
if (num % i == 0) // 如果i能够整除num,说明num不是素数
return false;
}
return true;
}
int n, k, a[22], res; // res用于存储答案
void dfs(int id, int cnt, int sum) {
if (cnt == k) {
if (check(sum)) res ++;
return;
}
if (id >= n) return;
dfs(id+1, cnt+1, sum+a[id]);
dfs(id+1, cnt, sum);
}
int main() {
cin >> n >> k;
for (int i = 0; i < n; i ++) cin >> a[i];
dfs(0, 0, 0);
cout << res << endl;
return 0;
}
当然,我们也可以进行一下下优化。
因为我们知道我们这里的搜索是使用的 深度优先搜索 算法。而深搜的最基础的优化是 剪枝 ,即:
既然我已经知道这么搜下去没有结果了,那么我就不会继续进行深度优先搜索了。
那么这里可以怎么剪枝呢?
我们假设当前我们在选择第 \(id\) 个数,此时一共选择了 \(cnt\) 个数。
那么这个时候我一共还有 \(n-id\) 个数可以选,而此时还需要选 \(k-cnt\) 个元素需要选,所以如果 \(n-id \lt k-cnt\) ,那么就选我接下来 \(n-id\) 个元素都选,我都凑不够 \(k\) 个数,所以我就不需要进行下去了。
这就是这里的深搜的剪枝优化(不过这道题目数据量比较小所以不剪枝也能过)。
实现的时候只需要在 dfs
函数的一开始加上如下这行代码即可:
if (n-id < k-cnt) return;
状态压缩+枚举解法
(注:请不要将 状态压缩 想得很难,它其实就是将一个数的二进制形式有一个状态之间进行一一对应)
我们知道深度优先搜索其实就是以递归的形式将情况进行了一遍枚举。
那么有没有别的办法进行枚举呢?
其实对于数组 \(a[0],a[1], \dots , a[n-1]\) ,我们可以从 \(0\) 到 \(2^n\) 去枚举每一个数(我假设我用一个变量 \(s\) 来表示这个数)。
那么我们可以知道这个 \(s\) 在二进制的情况下是刚好有 \(n\) 位的,它的第 \(i\) 位是否为1对应着是否选择了 \(a[i]\) :
- 如果 \(s\) 的二进制第 \(i\) 位为 \(1\) ,则表示选择了 \(a[i]\);
- 如果 \(s\) 的二进制第 \(i\) 位为 \(0\) ,则表示没有选择 \(a[i]\)。
于是,我们需要处理的细节就变成了:
- 判断 \(s\) 的二进制表示中是不是恰好有 \(k\) 位为 \(1\);
- 判断 \(s\) 的二进制表示中有哪些数位对应为 \(1\)。
对于第 \(2\) 个问题,假设我们想知道 \(s\) 的第 \(i\) 为是不是为1,我们只需要判断 s & (1<<i)
是否不为零;或者判断 (s>>i) & 1
是否不为零即可。
对于第 \(1\) 个问题,我们可以自己写一个函数来进行计算,比如这样:
bool check(int s) {
int cnt = 0;
for (int i = 0; i < n; i ++)
if (s & (1<<i)) // 或者 (s>>i) & 1 亦可
cnt ++;
return cnt;
}
但是C++也提供给了我们一个函数用于直接获得一个数对应的二进制中有多少位为1,就是“__builtin_popcount” 函数(注意:最前面是2个下划线横杠)。
它的使用方法如下(省略了头文件):
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << __builtin_popcount(3) << endl; // 3二进制11,所以输出2
cout << __builtin_popcount(9) << endl; // 9二进制1001,所以输出2
cout << __builtin_popcount(13) << endl; // 13二进制1101,所以输出3
return 0;
}
然后,我们就可以使用状态压缩的方式进行枚举,然后判断它们的和是不是素数就可以了。
实现代码如下:
#include <bits/stdc++.h>
using namespace std;
bool check(int num) { // 判断素数
if (num < 2) return false;
int a = sqrt(num); // 这里用sqrt的方式先求出来,不要将sqrt用在循环里面,因为数学函数比较慢
for (int i = 2; i <= a; i ++)
if (num % i == 0)
return false;
return true;
}
int n, k, a[22], res; // res用于表示答案
int main() {
cin >> n >> k;
for (int i = 0; i < n; i ++) cin >> a[i];
for (int s = 0; s < (1<<n); s ++) {
if (__builtin_popcount(s) == k) {
int sum = 0;
for (int i = 0 ; i < n; i ++) {
if (s & (1<<i))
sum += a[i];
}
if (check(sum)) res ++;
}
}
cout << res << endl;
return 0;
}