预备知识
1.集合的二进制表示
我们可以使用一个01串A来表示一个集合。对于数x(x≥0),用Ax=0表示它不在该集合中,用Ax=1表示它在该集合中。
将01串A看作是一个二进制数,我们把它转换为十进制,就可以使用一个十进制整数来表示一个实际使用二进制方式表示的集合。
这样,我们可以使用位运算方便地处理集合的操作。
2.集合的操作
·交集
两个集合A和B的交集,即为两个集合公有元素的集合。这等价于两个原始集合对应的二进制整数的某一位同时为1,则所求集合该位为1。
用位运算描述,交集运算实质上就是与运算,即A∩B=A&B。
·并集
两个集合A和B的并集,即为两个集合所有元素的集合。这等价于两个原始集合对应的二进制整数的某一位只要有一个为1,则所求集合该位为1。
用位运算描述,并集运算实质上就是或运算,即A∪B=A|B。
·判断包含关系
如果集合A是集合B的子集,那么集合A的每一个元素都属于集合B。那么,集合A与集合B的交集即为集合B,而它们的并集即为集合A。
用位运算描述,若(A&B)==B,则B⊆A(或若(A|B)==A,则B⊆A)。
·判断属于关系
设整数T=1<<x,则集合T=100…0(x个0),即只有x属于集合T。那么,如果集合T是集合A的子集,则x属于集合A。
用位运算描述,若((1<<x)|A)==A,则x∈A。
·补集
若集合A是集合U的子集,那么A在U中的补集为所有U中不属于A的元素的集合。由于A中的元素必定存在于U中,这等价于不同时存在于A和U但至少存在于其中一个集合的元素的集合,也就是异或运算的结果。
用位运算描述,若A⊆U,则CUA=A^U。
·差集
集合A与集合B的差集,即为属于集合A但不属于集合B的元素的集合。也就是说,我们从集合A中去掉同时属于A和B的元素,得到的就是A与B的差集,即它们的交集在A中的补集。
用位运算描述,A-B=A^(A&B)。
·对称差集
对于集合A与集合B,属于A或属于B,但不同时属于A和B的元素构成的集合称为它们的对称差集。这也就是它们异或运算的结果。
用位运算描述,A△B=A^B。
·全集生成
若要研究的元素范围在0~n之间,那么全集U为11…1((n+1)个1)。如何生成这样一个集合呢?我们知道,对于整数T=1<<n+2,对应的集合T=100…0((n+2)个0),则T-1=11…1((n+1)个1)。
用位运算描述,U=(1<<n+2)-1。
·元素生成
直接枚举元素范围0到n,判断每一个数是否是当前集合的元素即可。
·子集生成
直接枚举空集0到当前集合,判断每一个集合是否是当前集合的子集即可。
·标记为存在
将数x在集合A里标记为存在,首先构造出只有该元素的集合,再取它们的并集。
用位运算描述,A|=1<<x。
·标记为不存在
将数x在集合A里标记为存在,首先构造出只没有该元素的集合,再取它们的交集。
用位运算描述,A&=~(1<<k)。
理论概述
状态压缩动态规划,即状压DP,指的是把利用二进制方式表示的集合当作状态,在此基础上进行的动态规划。
例题
1.洛谷 P2622 关灯问题Ⅱ
·题目描述
现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。
现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。
·题目分析
按下某一个按钮,则所有的灯都会改变,从当前的状态改变为确定的另一个状态。对于从初始情况到这n盏灯的某一个状态,它的最小步数即为所有可能转移到该状态的状态最小步数加上1。
因此,我们可以将使用二进制方式表示的集合存储一个状态。我们把灯编号为1~n-1,那么,我们有状态转移方程f[A]=min{f[T]}+1(A为n盏灯的一个状态集合,T为可转移到该状态的一个状态集合),边界条件为f[(1<<n)-1]=0,即到达所有灯都开着的状态不需要按,结果为f[0],即所有灯都关上的状态。
实现的时候,对于每一个状态,枚举它可以到达的所有状态,判断是否可以转移即可。
·代码
1 #include<algorithm> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 const int MAXN=10; 6 int n,m,f[(1<<MAXN)+1],a[101][MAXN+1]; 7 int main() 8 { 9 scanf("%d%d",&n,&m); 10 for(int i=1;i<=m;++i) 11 for(int j=1;j<=n;++j) 12 scanf("%d",&a[i][j]); 13 memset(f,0x3f,sizeof(f)); 14 int ALL=(1<<n)-1; 15 f[ALL]=0; 16 for(int i=ALL;i>-1;--i) 17 for(int j=1;j<=m;++j) 18 { 19 int t=i; 20 for(int k=1;k<=n;++k) 21 if(a[j][k]==-1) 22 t|=1<<k-1; 23 else if(a[j][k]==1) 24 t&=~(1<<k-1); 25 f[t]=min(f[t],f[i]+1); 26 } 27 if(f[0]!=0x3f3f3f3f) 28 printf("%d",f[0]); 29 else 30 printf("-1"); 31 return 0; 32 }
2.洛谷 P2915 [USACO08NOV]奶牛混合起来Mixed Up Cows
·题目描述
约翰家有N头奶牛,第i头奶牛的编号是Si,每头奶牛的编号都是唯一的。这些奶牛最近 在闹脾气,为表达不满的情绪,她们在挤奶的时候一定要排成混乱的队伍。在一只混乱的队 伍中,相邻奶牛的编号之差均超过K。比如当K = 1时,1, 3, 5, 2, 6, 4就是一支混乱的队伍, 而1, 3, 6, 5, 2, 4不是,因为6和5只差1。请数一数,有多少种队形是混乱的呢?
·题目分析
我们考虑将当前选取的所有奶牛当做一个状态,而前一个选取的奶牛当做另一个状态。枚举所有的奶牛,如果它还没有被选取,且满足与上一个选取的奶牛差超过K,我们就将这个状态的方案数累加进下一个状态的方案数。其中边界为对于只有i的集合,最后选取的奶牛为i时,方案数为1。
·代码
1 #include<cstdio> 2 #include<cstring> 3 using namespace std; 4 long long n,k,ans,s[17],f[(1<<16)-1][17]; 5 int main() 6 { 7 scanf("%lld%lld",&n,&k); 8 for(long long i=1;i<=n;++i) 9 scanf("%lld",&s[i]); 10 long long ALL=(1<<n)-1; 11 for(long long i=1;i<=n;++i) 12 f[1<<i-1][i]=1; 13 for(long long i=1;i<=ALL;++i) 14 for(long long j=1;j<=n;++j) 15 for(long long l=1;l<=n;++l) 16 if((i|(1<<l-1))!=i&&(s[j]-s[l]>k||s[l]-s[j]>k)) 17 f[i|(1<<l-1)][l]+=f[i][j]; 18 for(long long i=1;i<=n;++i) 19 ans+=f[ALL][i]; 20 printf("%lld",ans); 21 return 0; 22 }