对于一个集合的子集有关的题,如果我们一个个枚举子集的情况,基本就是TEL了而且非常复杂
那么我们可以另辟蹊径,从二进制的角度讨论子集
假设一个数组A有五个元素1 2 3 4 5,A1{1,3,4,5},A2{1,4,5},A3 {3},A4 {2,3}都是它的子集
考虑某个顺序,把集合中出现的元素用1表示,未出现用0表示(二进制从后往前读)
A中元素 | 1 | 2 | 3 | 4 | 5 | 二进制 | 十进制 |
A1中出现的情况 | 1 | 0 | 1 | 1 | 1 | 11101 | a1=29 |
A2出现的情况 | 1 | 0 | 0 | 1 | 1 | 11001 | a2=25 |
A3出现的情况 | 0 | 0 | 1 | 0 | 0 | 00100 | a3=4 |
A4出现的情况 | 0 | 1 | 1 | 0 | 0 | 00100 | a4=6 |
那么可以发现子集A1可以表示为一个二进制数11101,对应十进制29,反之,这个数字可以表示子集A1。此时就发现了一种让自己对应于二进制的一种很直观地方法。
本列一共有五个元素,对于一个集合的第i(0<=i<=4)个元素,可以用s=1<<i表示;
对于一些子集之间的关系,有以下几种:
1、并集:显然A2并A3=A1;根据并集的数学意义,我们可以发现对应与二进制其实就是A1=A2|A3,二进制下的按位或运算符。
2、交集:显然A1交A4=A3;那我们同样可以清楚的发现对应与二进制其实就是A1&A4=A3,二进制下的按位与运算符。
3、包含:显然A1包含A2。A1并A2=A1,A1交A2=A2,那么如果要判断A1是否包含A2,可以写成
(a1|a2==a1&&(a1&a2==a2)),小写a代表用十进制表示集合A
4、属于:也就是检查某个元素是否在这个集合内,之前提到的某个元素的表示方法,可得,如果要判断第i个元素是否属于集合A1,可以写成1<<(i-1)&a1。
5、补集:相信大家都能猜到了,补集用的就是异或运算符^,啥意思呢?比如求A2关于A的补集,相当于a^a2;
说了这么多,我们用一道例题来体验体验
题目是洛谷P1036 [NOIP2002 普及组] 选数
题目描述
已知 nn 个整数 x_1,x_2,\cdots,x_nx1,x2,⋯,xn,以及 11 个整数 kk(k<nk<n)。从 nn 个整数中任选 kk 个整数相加,可分别得到一系列的和。例如当 n=4n=4,k=3k=3,44 个整数分别为 3,7,12,193,7,12,19 时,可得全部的组合与它们的和为:
3+7+12=223+7+12=22
3+7+19=293+7+19=29
7+12+19=387+12+19=38
3+12+19=343+12+19=34
现在,要求你计算出和为素数共有多少种。
例如上例,只有一种的和为素数:3+7+19=293+7+19=29。
输入格式
第一行两个空格隔开的整数 n,kn,k(1 \le n \le 201≤n≤20,k<nk<n)。
第二行 nn 个整数,分别为 x_1,x_2,\cdots,x_nx1,x2,⋯,xn(1 \le x_i \le 5\times 10^61≤xi≤5×106)。
输出格式
输出一个整数,表示种类数。
首先考虑如何枚举只含有k个元素的子集,很同意想到就是检查二进制下1出现的个数是否为k,当然很好统计,不过偷懒一下,统计二进制下1的个数可以用内建函数__builtin_popcount(),该函数返回一个数二进制下1出现的个数。
接下来找到喊k个元素的子集后,我们就需要检查每个数是否在这个子集内,用上文提到的属于的技巧,最后就是把这些书加起来判断是不是质数了。
枚举子集的算法时间复杂度是0(2的n次方),一般1秒可以枚举包含20到30个元素的集合的子集
#include<bits/stdc++.h>
using namespace std;
int a[30];
int prime(int x)
{
for (int i = 2; i * i <= x; i++)
if (x % i == 0)
return 0;
return 1;
}
int main()
{
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i++)
cin >> a[i];
int s = 1 << n;//s-1为全集
int ans = 0;
for (int i = 0; i < s; i++)
{
if (__builtin_popcount(i) == k)
{int sum = 0;
for (int j = 0; j < n; j++)
{
if (i & (1 << j))
sum += a[j];
}
if (prime(sum))
ans++;
}
}
cout << ans;
return 0;
}
题目描述
Perket 是一种流行的美食。为了做好 Perket,厨师必须谨慎选择食材,以在保持传统风味的同时尽可能获得最全面的味道。你有 nn 种可支配的配料。对于每一种配料,我们知道它们各自的酸度 ss 和苦度 bb。当我们添加配料时,总的酸度为每一种配料的酸度总乘积;总的苦度为每一种配料的苦度的总和。
众所周知,美食应该做到口感适中,所以我们希望选取配料,以使得酸度和苦度的绝对差最小。
另外,我们必须添加至少一种配料,因为没有任何食物以水为配料的。
输入格式
第一行一个整数 nn,表示可供选用的食材种类数。
接下来 nn 行,每行 22 个整数 s_isi 和 b_ibi,表示第 ii 种食材的酸度和苦度。
输出格式
一行一个整数,表示可能的总酸度和总苦度的最小绝对差。
#include<bits/stdc++.h> using namespace std; struct xx { int a, b; }p[12]; int main() { int n; cin >> n; for (int i = 0; i < n; i++) cin >> p[i].a >> p[i].b; int ans = 99999999; int w = 1 << n; for (int i = 1; i < w; i++)//这里写i=0会报错!!!!!!! { int sum1 = 1, sum2 = 0; for (int j = 0; j < n; j++) { if ((1 << j) & i) { sum1 *= p[j].a; sum2 += p[j].b; } } ans = min(ans, abs(sum1 - sum2)); } cout << ans; return 0; }