浅谈二进制状态压缩

引入

在做到某些搜索或dp题时,我们常常会发现如果用一个数组去存储一些仅有1和0的状态,在进行存储和更改时时间和空间都很可能会爆掉。这时候我们就要借助二进制进行状态压缩,使得一个01数组变成一个二进制数(以十进制存储),从而达到缩小时空复杂度的目的。

Ps:关于二进制状态压缩有一个误区:初学者常会认为我们需要将存储的十进制状态转化为二进制以进行修改操作,从而误认为状态压缩仅能缩小空间复杂度。其实,位运算能很好地解决这个问题。关于位运算修改状态的方法,在下文中会提及。


如何进行状压?

对于一个01数组\(a[]\),我们用一个二进制数替代它。例如,\(a[]={0,1,1,0,0}\),则进行状态压缩后的\(a=01100=12\),这样一来空间就缩小了许多。当然,二进制状压所能压缩的远不止5位二进制数。但是在进行压缩时,我们也要注意压缩后的数字不能超过int(long long)的范围。反过来说,在看到一些题目中\(n<=31\)\(m<=31\)时,我们可以很自然的想到二进制状态压缩。


一些基本的状态查询修改操作:

在进行状态查询和修改时,我们会用到位运算符号,如&(按位与),|(按位或),^(异或),<<,>>等。

这里要注意两点:

1.千万不要将&|这些按位运算符号打成&&||

2.由于位运算符号的优先级一般都很低,所以我们要养成随时加括号的习惯

接下来是一些基本的操作,在下文中\(a\)指当前状态,\(n\)为位数(证明很简单,略):

查询当前状态第\(i\)位是否为\(1\)a&(1<<i)

将当前状态第\(i\)位取反(\(1 \to 0\)\(0 \to 1\)):a^=(1<<i)

判断当前状态在二进制下是否有连续的若干个\(1\)(a&(a<<1))||(a&(a>>1))

当前状态二进制下取反:a^=((1<<n)-1)


例题:

[SCOI2005]互不侵犯

题意简明易懂,就不多做解释了。

那么这道题怎么做呢?

可以明确的一点是:这是一道状压dp

因为这篇文章主要讲的是状态压缩,所以dp方程就不具体推了

我们建立\(dp_{i,j,k}\)来存储第\(i\)行第\(j\)个状态,且放了\(k\)个国王的方案数

第一步:列出所有可能成立的单行状态

因为国王可以攻击到左右两格,所以可以通过上面的(a&(a<<1))||(a&(a>>1))

来排除一些不可能成立的状态

我们将所有成立的状态存储到一个数组里便于取用

注意全0状态也算进去

第二步:初始化

先列出第一行每一种状态的方案数,以便于后面的dp使用

第三步:dp

枚举\(i,j,l,k\),这里\(l\)指第\(i-1\)行的状态是第\(l\)种,\(j\)指第\(i\)行的状态是第\(j\)种,并且排除掉上下行的国王可以互相攻击到的情况

\(a\)代表\(j\)状态,b代表\(l\)状态

排除方法:if((a&b)||(a&(b<<1))||(a&(b>>1)))continue;

接下来再通过\(dp_{i,j,k}+=dp_{i-1,l,k-sum(a)}\)的dp方程求出最终要求的方案数,这里\(sum(a)\)指a在二进制下\(1\)的个数

code:

#include <cstdio>
#include <iostream>
using namespace std;
int n,k,a,b,tot,use[1001];
long long dp[11][1001][101];
long long ans;
int sum(int s){
    int num=0;
    while(s){
        if(s%2)num++;
        s/=2;
    }
    return num;
}
int main(){
    scanf("%d%d",&n,&k);
    for(int i=0;i<=(1<<n)-1;i++)
    if(!(i&(i>>1)))use[++tot]=i;
    for(int i=1;i<=tot;i++){
        dp[1][i][sum(use[i])]=1;
    }
    for(int i=2;i<=n;i++){
        for(int j=1;j<=tot;j++){
            for(int t=1;t<=tot;t++){
                a=use[j],b=use[t];
                if((a&b)||(a&(b<<1))||(a&(b>>1)))continue;
                for(int p=sum(a);p<=k;p++){
                    dp[i][j][p]+=dp[i-1][t][p-sum(a)];
                }
            }
        }
    }
    for(int i=1;i<=tot;i++){
        ans+=dp[n][i][k];
    }
    printf("%lld\n",ans);
    return 0;
} 

最后别忘了long long哦~


类型题推荐:

[USACO06NOV]玉米田Corn Fields

[NOI2001]炮兵阵地

如有问题和错误,请各位尽情提出,感激不尽~

posted @ 2019-06-14 23:57  huangxuanao  阅读(2369)  评论(0)    收藏  举报