P1036 [NOIP 2002 普及组] 选数
分析:
本题目可以认为是从一个有 \(n\) 个数字的数字的集合中挑选出一些数字(也就是子集),然后判断该子集是否满足某个性质(其和是质数)。集合枚举的意思是从一个集合中找出它的所有子集。集合中每个元素都可以被选或不选,含有 \(n\) 个元素的集合共有 \(2^n\) 个子集(包括全集和空集)。
考虑集合 \(A=\{1,2,3,4,5\}\) 和它的 \(4\) 个子集 \(A_1=\{1,3,4,5\}、A_2=\{1,4,5\}、A_3=\{3\}、A_4=\{2,3\}\)。按照某个顺序,把全集 \(A\) 中的每个元素在每个子集中的出现状况用 \(0\)(没出现)和 \(1\)(出现了)表示出来,见下表:
集合 \(A\) 中元素在子集中的出现状况
| \(A\) 中元素 | \(1\) | \(2\) | \(3\) | \(4\) | \(5\) | 二进制 | 对应十进制 |
|---|---|---|---|---|---|---|---|
| 在 \(A_1\) 中的出现情况 | \(1\) | \(0\) | \(1\) | \(1\) | \(1\) | \(11101\) | \(a_1=29\) |
| 在 \(A_2\) 中的出现情况 | \(1\) | \(0\) | \(0\) | \(1\) | \(1\) | \(11001\) | \(a_2=25\) |
| 在 \(A_3\) 中的出现情况 | \(0\) | \(0\) | \(1\) | \(0\) | \(0\) | \(00100\) | \(a_3=4\) |
| 在 \(A_4\) 中的出现情况 | \(0\) | \(1\) | \(1\) | \(0\) | \(0\) | \(00110\) | \(a_4=6\) |
那么,可以发现 \(A\) 的子集 \(A_1\) 可以表示为一个“二进制数”\(11101\),对应十进制 \(a_1=29\);反之,这个数字也可以表示子集 \(A_1\)。注意,这边的集合是大写字母,集合对应的数字是小写字母。同理,\(A_2\) 可以表示为二进制数 \(11001\),\(A_3\) 可以表示成 \(00100\),\(A_4\) 可以表示成 \(00110\)。此时找到了一种让子集对应于二进制数的很直观的方法,但是这种表式方法的威力不仅限于此。
本例一共有 \(5\) 个元素,表示仅包含第 \(i(1 \leq i \leq 5)\) 个元素的集合的数字可以使用按位移运算构造,写成 1<<(i-1);而包含所有元素的全集可以表示成 a=(1<<n)-1,空集表示为 \(0\)。此外,集合之间存在一些联系。集合的常用关系有下面几种。
- 并集:从元素选择角度来说,就是 \(A_2、A_3\) 包含的元素合并起来能够得到 \(A_1\)。可以发现,\(A_1\) 的每一位都等于对应位 \(a_2\) \(or\) \(a_3\) 的结果,可以写成
a1=s2|a3。只需要把两个集合的二进制数进行按位或运算即可表示两个集合的并集。 - 交集:是指两个集合中同时存在的元素组成的集合,从逻辑上推导出交集含有“与”这个逻辑。根据上面的并集运算,不难猜出:表示两个子集的交集时,可以把表示两个子集的二进制数进行按位与运算,即
a1=a2&a3(表示 \(a_2\) 与 \(a_3\) 的并集)。 - 包含:集合 \(A_2\) 的所有元素都在 \(A_1\) 中出现,说明 \(A_1\) 包含 \(A_2\)。容易得到 \(A_1 \cup A_2=A_1\),同时 \(A_1 \cap A_2=A_2\),写成判断条件即
a1|a2==a1&&a1&a2==a2。 - 属于:是指某个元素(假设是第 \(n\) 个)在集合中,是包含的一种特殊情况 —— 只需检查表示这个集合的二进制数从右往左(从低位到高位)第 \(n-1\) 位是否为 \(1\)。可以写成
1<<(n-1)&a1或者a1>>n&1。 - 补集:是指全集(\(A_1\))去除了某个集合(\(A_2\))中包含的所有元素后剩下元素所组成的集合。很显然 \(A_2\) 的补集中所包含的元素是只属于 \(A_1\) 并且不属于 \(A_2\) 的,相当于按位异或运算,可以写成
a1^a2。
回到题目。本题可以枚举由 \(n\) 个元素组成的集合中含有 \(k\) 个元素的子集。如何判断一个数在二进制下恰好有 \(k\) 个 \(1\)?可以用内建函数 __builtin_popcount(),它能直接返回一个数在二进制下 \(1\) 的个数;当然也可以自己手写函数。
在找到由 \(k\) 个元素组成的子集后,把它包含的数全部加起来,判断和是否是质数即可。枚举子集的算法时间复杂度为 \(O(2^n)\),\(1\) 秒钟内最多可以枚举 \(30\) 个元素的子集,在本题范围内。代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,k,s,ans,a[30];
bool check(int x){//判断质数
if(x<2)return 0;
for(int i=2;1ll*i*i<=x;i++){//i*i乘上1ll,防止爆int
if(x%i==0){
return 0;
}
}
return 1;
}
int pc(int x){//相当于__builtin_popcount()
int cnt=0;
while(x){
cnt++;
x&=x-1;//消除x二进制下的最后一个1
}
return cnt;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<1<<n;i++){//枚举子集[1,(1<<n)-1]
if(pc(i)==k){//找到了由k个元素所组成的子集
s=0;
for(int j=0;j<=n;j++){//j从0开始,对应第j+1个元素
if(1<<j&i){//也可以写成i>>j&1
s+=a[j+1];
}
}
if(check(s)){
ans++;
}
}
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号