一、
集合可以用二进制表示,二进制从低到高第i位为1表示i在集合中,为0表示i不在集合中。例如集合{0,2,3}可以用二进制数1101表示。反之,1101对应集合{0,2,3}
那么,集合就可以被压缩成一个数字S,例如{0,2,3}可以被压缩成2^3+2^2+2^0=13.
二、集合与集合:
求交集:a&b.理由:第i位表示i在集合中。交集表示公共部分。只有a与b的第i位都为1,才意味着i可以成为交集的一部分。
求并集:a|b.理由同上。只要两个集合中的其中一个有,就可以成为并集的一部分。
求对称差:解释一下:两个集合的对称差是只属于其中一个集合而不属于另一个集合的元素组成的集合,也就是不在交集中的元素组成的集合。显然是异或。a^b.你有我没有,或者我有你没有,才符合题意。
求差:解释一下:集合A与集合B的差表示:A中有但B中没有的元素的集合,B中有但A中没有的元素不算进去。求差的方法为:a&~b.理由:~b中第i位为1表示i不在B中,而A-B就是要找在A中但不在B中的元素。i在A中则a的第i位为1,i不在b中则~b的第i位为1,两者按位与即可。
包含于:A包含于B等价于:a&b==a或a|b==b.解释:a&b==a意味着a中为1的位置在b中对应的位置也为1,意即A有的元素B也有,反之a中为0的位置在b中可能为1也可能为0,因为我们进行的是按位与,所以无论是1还是0,和0进行按位与后都会变成0。a|b==b同理。a中为1的位置在b中也为1(因为如果b中这个位置为0,那么按位或之后就会变成1,那就意味着a|b!=b了),a中为0的位置在b中可能为0也可能为1,不管是0还是1,和0进行按位或之后都是它本身。
三、集合与元素
单元素集合{i}:二进制表示为1<<i.这是显然的。因为只有一个元素i,也就意味着在它的二进制表示中只有第i位为1,而最低位是第0位(因为0也可能在集合里),所以只需要将1左移i位。例如集合{2}的二进制表示为:100
全集U={0,1,2,......,n-1}:(1<<n)-1.理由:第0位到第n-1位都为1的二进制数+1之后变成只有第n位为1的二进制数。
集合S关于全集U的补集:((1<<n)-1)^s.理由:补集:S没有且U有的元素。对应到二进制表达上就是:s的第i位为0且U中的第i位为1
i属于集合S:(s>>i)&1==1.理由:将s右移i位后的最低位就是原来的第i位,将该位与1作按位与运算,如果结果为1,表示原来的第i位是1,意即i在集合S中,反之亦然。
i不属于集合S:(s>>i)&1==0.理由同上
向集合S中添加元素i:s|(1<<i).理由:1<<i:第i位为1.无论s的第i位是0还是1,进行按位或运算后都会变成1.代表着i已存在于S中。
删除集合S中的元素i:s&~(1<<i).理由:1<<i:第i位为1,取反后前i-1位都为0,第i位为0。如果在s中第i位为1(即i存在于s中),那么与~(1<<i)做按位与运算后结果为0。
删除最小元素:s&(s-1).理由:s-1的功能:将s中最低位的1变成0,同时最低位1右边的0全都变成1。我们假设s中最低位1在第i位,这意味着s中的最小元素为i.s&(s-1),则第i位的1必然变成0.
计算集合大小:即求二进制中1的个数。可以使用库函数,也可以自己实现。
int popcount(unsigned int x)//计算集合大小
{
int ans = 0;
while (x)
{
ans += (x & 1);
x >>= 1;
}
return ans;
}
计算二进制长度:
int bitlength(unsigned int x)//计算二进制长度
{
int ans = 0;
while (x)
{
ans++;
x >>= 1;
}
return ans;
}
计算集合最大元素:二进制长度减一就是集合最大元素。因为最低位为第0位。
计算集合最小元素:即求集合最小元素。
int minelement(unsigned int x)
{
int ans = 0;
while(x&0)
{
ans++;
x >>= 1;
}
return ans;
}
三、遍历集合
for (int i = 0; i < n; i++)
{
if ((s >> i) & 1)//如果i在s中
{
//处理i
}
}
四、枚举集合
1.枚举所有集合:设元素范围从0到n-1
for (int s = 0; s < (1 << n); s++)
{
//处理s
}
理由:1<<n:第n位为1,其它位置都为0.意味着只有n在集合中。而我们的元素范围是从0到n-1.因此全集为:(1<<n)-1.
2.枚举非空子集
for (int sub = s; sub; sub = (sub - 1) & s)
{
//处理sub
}
我们来模拟一下。
sub-1:把sub中的最低位1改成0,并把最低位1后面的所有0都改成1.一开始sub==s,那么第一次操作相当于(s-1)&s.由上文可知,这相当于删除了s中的最小元素。此时sub为s的子集,是s去掉最小元素后的集合。
继续循环:sub=(sub-1)&s.sub-1:把sub中最低位1改成0,并把最低位1后面的0都改成1.由上可知,此时sub中最低位1表示s中的次小元素。改为0后再与s按位与,必然为0.而sub原本已经去掉了s中的最小元素,-1后该位会恢复为1,与s按位与后该位必然为1.此时sub为s的子集,是s去掉次小元素的集合。
同理,一直重复这个过程。直到sub成为s去掉最大元素的集合。在这个过程中,sub的大小是s的大小-1(除了刚开始的sub=s)。
接着继续循环。由于sub是s去掉最大元素的集合,而在接下来的循环中,我们又不断去掉sub的最小元素、次小元素......,所以在这个过程中sub的大小是s的大小-2,直到最后我们去掉了sub的最大元素。
然后继续这个过程。sub的大小为s的大小-3...如此循环,即可枚举完s的所有非空子集。
这也是为什么我们要从sub=s开始枚举。这样我们就可以通过for循环控制子集的大小。当枚举完一定大小的子集后,会自动“去头”。
3.枚举子集(包含空集)
int sub = s;
do
{
//处理sub
sub = (sub - 1) & s;
} while (sub!=s);
4.枚举超集
如果T是S的子集,那么S是的超集。
for (int s = t; s < (1 << n); s = (s + 1) | t)
{
//处理s
}
枚举超集的逻辑和前面枚举子集的逻辑是一样的。
我们还是来模拟一遍:
s+1:把s最低位0改成1,并把最低位0后面的1都改成0.
一开始s==t,s+1后再与t按位或,相当于把最低位的0改成1,然后最低位的0后面的1保持不变。此时s为t的超集,假设最低位0的位置是第i位,那么此时S就是T加上第i位元素构成的集合。
然后再+1,把t中次低位的0改成1,后面的1改成0,与t按位或,由于t中第i位为0,且+1后s的第i位也为0,因此按位或之后该位还是保持为0,假设次低位为0的位置是第j位,那么此时S就是T加上第j位元素构成的集合。
不难发现,在这个过程中,S的大小保持不变,都为T的大小+1.和前面枚举子集的逻辑是一样的。
假设t中最高位0的位置为第k位,当S为T加上第k位元素构成的集合,s+1只会从低位开始加起,也就是说接下来的过程中,S的大小会保持T的大小+2不变。接下来的逻辑同上。