反转(开关问题)
参考《挑战程序设计》
https://blog.csdn.net/codeswarrior/article/details/80228454
https://blog.csdn.net/sky_zdk/article/details/60320721
集合与二进制
在程序中表示集合的方法有很多种,当元素数比较少时(集合元素的个数不超过32位时可以用int,不超过64时可以用long long),像这样用二进制码来表示比较方便。集合{0,1,2…n-1}的子集可以用以下方式编码成整数。
举一个比较直观的例子:
比如一个集合(5,8,3,6,78,45,33,22),我们可以用一个n位的2进制数表示这个集合。
比如11111111代表这个全集,而10100000代表(5,3)这个子集(二进制中1代表这个子集中有全集中相应位置的数字)
像这样表示后,一些集合运算可以对应地写成如下方式:(全集按6个元素)
空集......................................................................0
只含有第i个元素的集合{i}.....................................1<<i (1<<2=4(10进制)=000100(2进制),1<<4=010000)
只含有全部n个元素的集合{0,1,2,....n-1}...............(1<<n)-1 (1<<6=1000000 1<<6-1=0111111)
判断第i个元素是否属于集合S...............................if(S>>i&1)
向集合中加入第i个元素SU{i}................................S|1<<i
从集合中去除第i个元素S\{i}..................................S&~(1<<i)
集合S和T的并集....................................................S|T
集合S和T的交集....................................................S&T
将集合{0,1,2,....n-1}所有子集枚举出来,按照这个顺序进行循环的话,S便会从空集开始循环,然后按照{0}、{1}、{0,1}、…、{0,1,…,n-1}的升序枚举出来。
for(s=0;s<1<<n;s++)
{//接下来的操作。
}
枚举某个集合(这个集合本身是别人的子集 比如0111000)的子集。这里sup 是一个二进制码,其本身也是某个集合的子集。例如给定了01101101 这样的集合,要将01100000 或者00101101 等子集枚举出来。前面是从0 开始不断加1 来枚举出了全部的子集。此时,sub+1 并不一定是sup 的子集。而(sub+1)&sup 虽然是sup 的子集,可是很有可能依旧是sub,没有任何改变。所以我们要反过来,从sup 开始每次减1 直到0 为止。由于sub-1 并不一定是sup 的子集,所以我们把它与sup 进行按位与&。这样的话就可以将sup 所有的子集按照降序列举出来。(sub-1)&sup 会忽略sup 中的0 而从sub 中减去1。
int sub = sup;
do {
// 对子集处理
// print_subset(n,sub);
sub = (sub-1) & sup;
} while(sub != sup); // 处理完之后 会有 -1&sup = sup
枚举{0,1,2....n-1}所包含的所有大小为k的子集
int comb = (1<<k)-1;
while (comb< 1<<n)
{
// print_subset(n,comb);
//这里进行针对组合的处理
int x=comb&(-comb), y=comb+x;
comb=((comb&~y)/x>>1)|y;
}
按照字典序的话,最小的子集是(1<< k)-1,所以用它作为初始值。现在我们求出comb其后的二进制码。例如0101110之后的是0110011,0111110之后的是1001111。下面是求出comb下一个二进制码的方法。
(1)求出最低位的1开始的连续的1的区间。
(0101110 -> 0001110)
(2)将这一区间全部变为0,并将区间左侧的那个0变为1。
(0101110 -> 0110000)
(3)将第一步里取出的区间右移,直到剩下的1的个数减少了一个。
(0001110 -> 0000011)
(4)将第二步和第三步中的结果按位取或(0110000|0000011 = 0110011)
对于非零的整数, x&(-x)的值就是将其最低位的1独立出来后的值,这部部分内容在树状数组部分有详细讲解,这里就不赘述了。
将最低位的1取出后,设它为x。那么通过计算y = comb + x,就将comb从最低位的1开始的连续的1都置零了。我们来比较一下~y和comb。在comb中加上x后没有变化的位,在~y中全都取相反的值。而最低位1开始的连续区间在~y中依然是1,区间左侧的那个0在~y中依然也是0.于是通过计算z = comb & ~y 就得到了最低位1开始的连续区间。比如:如果comb = 0101110,则 x = 0000010,y = 0110000,z = 0001110.
同时,y也恰好是第二步要求的值。那么首先将z不断右移,直到最低位为1,这通过计算z/x即可完成。这样再将z/x右移1位,就得到了第三步要求的值。这样我们就求得了comb之后的下一个二进制列。因为是从n个元素的集合中进行选择,所以comb的值不能大于等于
1*2^n,如此一来,就完成了大小为k的所有子集的枚举。
代码如下:
#include<cstdio>
#include<iostream>
using namespace std;
int a[10];
//打印集合元素
void print_subset(int n,int b) //n位的集合,b状态打印
{
printf("{ ");
for (int i=0;i<n;i++)
if (b&(1<<i)) printf("%d ",a[i]);
printf("}\n");
}
void p_s(int n)
{
for (int i=0;i<(1<<n);i++) print_subset(n,i); //枚举所有状态
}
int main()
{
int n;
scanf("%d",&n);
//枚举该集合的所有子集
for (int i=0;i<n;i++) a[i]=i;
p_s(n);
//只含有第i个元素的集合{i}
print_subset(n,1<<2);
//含有n个元素的集合
print_subset(n,(1<<n)-1);
//从集合中去除第i个元素S\{i}
print_subset(n,((1<<n)-1)&~(1<<1));
//求两个集合的交集
print_subset(n,1<<1|1<<2);
cout<<"子集的子集:" <<endl;
int sup=(1<<n)-3;
int sub = sup;
do {
// 对子集处理
print_subset(n,sub);
sub = (sub-1) & sup;
} while(sub != sup); // 处理完之后 会有 -1&sup = sup
cout<<"大小为k的大小:" <<endl;
int comb = (1<<3)-1;
while (comb< 1<<n)
{
print_subset(n,comb);
//这里进行针对组合的处理
int x=comb&(-comb), y=comb+x;
comb=((comb&~y)/x>>1)|y;
}
return 0;
}
如果一个元素只有两种状态,枚举所有状态的代码如下。
#include<iostream>
#include<string.h>
using namespace std;
//flip第一行可以枚举所有状态
/*
i:0 0 0 0 0
i:1 1 0 0 0
i:2 0 1 0 0
i:3 1 1 0 0
i:4 0 0 1 0
i:5 1 0 1 0
i:6 0 1 1 0
i:7 1 1 1 0
i:8 0 0 0 1
i:9 1 0 0 1
i:10 0 1 0 1
i:11 1 1 0 1
i:12 0 0 1 1
i:13 1 0 1 1
i:14 0 1 1 1
i:15 1 1 1 1
*/
int main(){
int flip[4][4];
int N=4;
for(int i=0; i<1<<N; i++)//1<<N: 2^n
{
cout<<"i:"<<i<<" ";
memset(flip,0,sizeof(flip));
for(int j=0; j<N; j++)
{
flip[0][N-j-1]=i>>j&1;
}
for(int j=0; j<N; j++) {
cout<<flip[0][N-j-1]<<" ";
}
cout<<endl;
}
return 0;
}

浙公网安备 33010602011771号