状压dp
概述
状压dp就是用二进制表示状态,并且对其子集进行归并的动态规划。它基于基本的位运算,常出现在NOIP和省选中。
状压有明显的数据范围暗示,一般在20左右
三进制枚举
有时我们需要先枚举一个子集,再枚举这个子集的一个子集,此时我们经常二进制枚举两层再判断是否包含。这样很直接,但不如三进制枚举快速。
假设我们有集合$S$,我们需要$A\subseteq S$,$B\subseteq A$,此时$S$集合的元素有三种情况:只在$S$中;既在$S$中又在$A$中;同时在三个集合中。只有三种情况,于是我们可以三进制枚举。
三进制枚举模式代码:
for (int x = 0; x < (1 << n); ++x) {
for (int y = x;; y = (y - 1) & x) {
for (int i = 0; i < n; ++i) {
if ((x >> i) & 1) {
// Do something
}
if ((y >> i) & 1) {
// Do something
}
}
// Do something
if (y == 0) break;
}
}
代码中关键的语句是y = (y - 1) & x
,-1
保证状态不同且最接近原状态&
保证是子集。
例题:P1896 [SCOI2005]互不侵犯
题意很简单,就是在$N\times N$的棋盘中放$K$个国王,使得国王之间不互吃。
我们很容易想到状压dp,我们按行枚举,再枚举上一行的状态,再枚举这一行的状态,再枚举价格,转移即可。
对于两层枚举状态,我们可以使用两层二进制枚举,也可以使用上面的三进制枚举。其中三进制枚举复杂度更低,也不需要检验正确性,其中$S$集合为所有状态,$A$集合为这一行不会被上一行影响的部分,$B$集合为当前行的状态。
这应该是此题除了打表最快的做法了。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF=1e9+7,MAXN=10,MAXP=1<<9,MAXK=85;
int N,K,maxp,ilg[MAXP]/*illegal*/,cnt[MAXP];
LL f[MAXN][MAXP][MAXK],ans;
int main(){
scanf("%d%d",&N,&K);
maxp=1<<N;
for(int i=0;i<maxp;i++){
for(int j=0;j<N;j++){
if((i>>j)&1){
cnt[i]++;
if(((i>>j)&3)==3)
ilg[i]=1;
}
}
f[1][i][cnt[i]]=!ilg[i];
}
for(int i=2;i<=N;i++)/*line*/
for(int j=0;j<maxp;j++){/*last*/
if(ilg[j])
continue;
int no=((maxp-1)^(j|(j<<1)|(j>>1)))%maxp;
for(int k=no;;k=(k-1)&no){/*current*/
if(ilg[k])
continue;
if(k>=maxp)
return 0;
for(int l=0;l+cnt[k]<=K;l++)
f[i][k][l+cnt[k]]+=f[i-1][j][l];
if(!k)
break;
}
}
for(int i=0;i<maxp;i++)
ans+=f[N][i][K];
printf("%lld",ans);
return 0;
}
枚举排列
对于一个排列,我们有时可以对于它的子集进行操作,此时可以使用状压dp。
但有时我们面对的不是一个排列,而是有重复元素的数列,此时我们需要按照以下公式去重(用多重集合的排列) $$\frac{n!}{\prod n_i!}$$ 其中$n_i$为每种元素的个数
例题:P4163 [SCOI2007]排列
来源:oschina
链接:https://my.oschina.net/u/4359259/blog/3441682