状压dp

拥有回忆 提交于 2021-02-18 01:03:18

状压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]排列
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!