集合子集的枚举(与二进制的关系)

对于一个集合的子集有关的题,如果我们一个个枚举子集的情况,基本就是TEL了而且非常复杂

那么我们可以另辟蹊径,从二进制的角度讨论子集

假设一个数组A有五个元素1 2 3 4 5,A1{1,3,4,5},A2{1,4,5},A3 {3},A4 {2,3}都是它的子集

考虑某个顺序,把集合中出现的元素用1表示,未出现用0表示(二进制从后往前读)

A中元素12345二进制十进制
A1中出现的情况1011111101

a1=29

A2出现的情况1001111001a2=25
A3出现的情况0010000100a3=4
A4出现的情况0110000100a4=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;
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值