状压dp

状态压缩DP

[TOC]

1.状态压缩的定义

状态压缩的定义:我们知道任何一个二进制都可以对应唯一的十进制数,反过来也成立。所以我们可以用一个数来代替一组数从而降低维数。这种解题手段我们叫做状态压缩。

举个例子:如果数组中的某一行全是0或全是1,例如000,001,我们可以将000用0表示,001用1来表示,这样一来就降低了维数,起到了状态压缩的作用。状态压缩dp就是基于此的dp。

2.位运算

状压dp与位运算关系密切,所以先了解一下位运算。

基本的一些位运算(如&,|,^,~,<<,>>)的基本用法就不多做介绍。来说一下它们的一些运用:

1:判断一个数x的二进制下第 i 位是不是1:(x & 1 << i)

2:将一个数x的二进制下第 i 位改为1:(x = x | (1 << i - 1))

3:把一个数二进制下最靠右的第一个1去掉:(x &= x - 1)

......待补充

3.入门例题

1:poj-3254

题意:一个n*m的草地,1代表有草,0有没。两个草不能相邻,问有多少种选法。

状态:dp[i][state[j]]表示前 i 行,第 i 行采用第 j 个状态时得到的总方案数。所以可得状态转移方程:dp[i][state(j)]=dp[i-1][state(k1)]+dp[i-1][state(k2)]+......+dp[i-1][state(kn)]

show code:

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int mod=1e8;
const int maxn=1e5+10;
int state[maxn],dp[15][maxn];
int arr[15][15],n,m,tot;
void init()                     //求出合理状态打个表
{
    int maxlen=1<<12;
    for(int i=0;i<maxlen;++i){
        if((i&(i<<1))==0)    state[++tot]=i;    //与运算要打括号!!!
    }
}
bool check(int row,int x)
{
    for(int i=1;i<=m;++i)
    {
        if(x&(1<<(i-1))&&!arr[row][i])      //检查一下x的所有位是否与已求出的合法状态冲突
            return false;
    }
    return true;
}

int main()
{
    init();
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(int i=1;i<=n;++i)
            for(int j=1;j<=m;++j)
                scanf("%d",&arr[i][j]);
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;++i)
        {
            for(int j=1;state[j]<(1<<m);++j)
            {
                if(!check(i,state[j]))  continue;      //保证下面的情况都与输入不冲突
                if(i==1){
                    dp[i][j]=1;
                    continue;
                }
                for(int k=1;state[k]<(1<<m);++k){     //根据状态转移方程而来
                    if((state[k]&state[j])==0)        //同一列没有两个1,两个数做与运算一定要打括号!!!!
                        dp[i][j]+=dp[i-1][k];
                }
            }
        }
        /*for(int i=1;i<=n;++i){
            for(int j=1;state[j]<(1<<m);++j)
                printf("%d ",dp[i][j]);
            printf("
");
        }*/
        ll res=0;
        for(int i=1;state[i]<(1<<m);++i)
            res=(res+dp[n][i])%mod;
        printf("%d
",res);
    }
    return 0;
}

2:poj-3311

题意:一个有向图,一个点可以走多次,问从0号点开始走把所有点都走一遍且最后回到原点的最短路径是多少。

#include<bits/stdc++.h>

using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=15;
int arr[maxn][maxn];
int n,dp[1<<11][maxn];

int main()
{
    while(scanf("%d",&n)&&n)
    {
        for(int i=0;i<=n;++i)
            for(int j=0;j<=n;++j)
                scanf("%d",&arr[i][j]);
        for(int k=0;k<=n;++k)                 //预处理求出任意两点之间的最小距离
            for(int i=0;i<=n;++i)
                for(int j=0;j<=n;++j)
                    if(arr[i][k]+arr[k][j]<arr[i][j])
                        arr[i][j]=arr[i][k]+arr[k][j];
        for(int s=0;s<(1<<n);++s)       //s表示状态
        {
            for(int i=1;i<=n;++i)                       //状态要从1开始表示,但其实是第i+1号节点(从1开始编号的话)
            {
                if(s&(1<<(i-1)))
                {
                    if(s==(1<<(i-1)))           dp[s][i]=arr[0][i]; //如果刚好只到城市i,也是边界情况
                    else{
                        dp[s][i]=inf;
                        for(int j=1;j<=n;++j)                       //找一个合适的点使转过去之后距离最小
                            if ((s>>(j-1))&1 && j!=i )
                                dp[s][i]=min(dp[s][i],dp[s^(1<<(i-1))][j]+arr[j][i]);
                                //在s状态没有经过城市i中找一点,使得距离更短
                    }
                }
            }
        }
        int res=dp[(1<<n)-1][1]+arr[1][0];
        for(int i=2;i<=n;++i)
            res=min(res,dp[(1<<n)-1][i]+arr[i][0]);
        printf("%d
",res);
    } s
}

3:poj 1185

题意:m*n的一块地, P表示平原, H表示山地, P上可以部署炮车, 炮车的攻击范围为前后左右范围内2个单位的十字行, 不能斜的攻击, 问最多能放多少个炮车。n : 100, m : 10;

因为每行只有10列, 故可用一个十进制数将所有状态都表示出来, 注意约束条件:①:山地不能放炮车, ②:炮车之间不能相互攻击;故可设$$f[i][j][k]$$表示第 i 行的状态为 j, 上一行的状态为 k 的可部署的最多炮车的数量。直接开100行数组会爆内存,加个滚动数组优化就行了

#include <bits/stdc++.h>
#define debug(x) cout << #x << " = " << x << endl; 

using namespace std;
typedef long long LL;
const int maxn = 1 << 11;
const int inf = 0x3f3f3f3f;
const double eps = 1e-6, pi = acos(-1.0);
int gcd(int a,int b)    { return b == 0 ? a : gcd(b, a % b); }

int n, m;
int f[2][maxn][maxn], g[110];
char s[110][11];
vector<int> state;

bool ok(int x){
    for(int i = 0; 1 << i <= x; ++i){
        if((x & 1 << i) && ((x & 1 << i + 1) || (x & 1 << i + 2))) return 0;
    }
    return 1;
}

int main()
{
    scanf("%d %d", &n, &m);
    for(int i = 0; i < n; ++i){
        scanf("%s", s[i]);
        for(int j = 0; j < m; ++j){
            if(s[i][j] == 'P')  continue;
            g[i] += 1 << j;
        }
    }
    for(int i = 0; i < 1 << m; ++i){    //打表, 将符合条件的状态存起来
        if(ok(i))   state.push_back(i);
    }
    int len = state.size();
    for(int i = 0; i <= n + 1; ++i){    //阶段: 枚举到哪一行了
        for(int j = 0; j < len; ++j){    //状态:上山一行的状态
            for(int k = 0; k < len; ++k){    //状态:上一行的状态
               for(int u = 0; u < len; ++u){    //状态:这一行的状态, 顺序可以乱
                    int now = state[u], pre = state[k], ppre = state[j];
                    if((now & pre) || (now & ppre) || (pre & ppre)) continue;
                    if(g[i] & now) continue;
                    f[i&1][now][pre] = max(f[i&1][now][pre], f[i-1&1][pre][ppre] + __builtin_popcount(now));
                    //决策, 这个状态可以从上一行的状态转移而来
                }
            }
        }
    }
    printf("%d
", f[n+1&1][0][0]);
}
原文地址:https://www.cnblogs.com/StungYep/p/12254167.html