状压dp入门(含例题)

介绍:

  • 状压,顾名思义,就是采用位运算,来记录更多的必须记录的状态
  • 状压dp一般会有明显的数据范围特征,即N,M一般都在20以内。可将每一排的N个看成一个N位二进制,先预处理出每一行可以运行的状态,这样可以去掉很多无效状态,然后dp处理,枚举当前有效状态和上一行有效状态的关系。

位运算:

^:不同为1,相同为0
~:按位取反
x >> 1(去掉最后一位 ,即除以2)
x << 1(在最后加一个0 ,即乘以2)
if( ((1<<(i−1))&x)>0 )判断一个数字x二进制下第i位是不是等于1(最低第1位)
x=x|(1<<(i−1))将一个数字x二进制下第i位更改成1
x=x&( ~(1<<(i−1)) )将一个数字x二进制下第i位更改成0
x=x&(x−1)把一个数字二进制下最靠右的第一个1去掉
x ^ 1最后一位取反

… … 更多操作自己学吧 (遇到不会的自然就去学了QAQ)

例一:

[SCOI2005]互不侵犯KING

要点:

  • f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示第 i i i行的状态为 j j j(用01串表示)且已经放了 k k k个国王(这行及之前行加起来)的方案数
  • f [ i ] [ j ] [ k ] + = f [ i − 1 ] [ t ] [ k − n u m [ j ] ] f[i][j][k]+=f[i-1][t][k-num[j]] f[i][j][k]+=f[i1][t][knum[j]](t是所枚举的第(i-1)行的状态)
  • f[0][1][0]=1;初始化第0行放0个国王的方案数(这里可以理解为不同种类数)为1
  • 检查 i i i这种状态是否冲突:if(i&(i<<1))continue;(让除第一位外的每一位和其前一位去比较,保证这行没有相邻的两个1,i&(i>>1)也可以)
  • 统计 i i i这种状态放置国王的数量:for(j=0;j<N;j++) if(i&(1<<j))k++;(遍历每位看是否是1)
  • 枚举每行状态for(i=0;i<(1<<N);i++)(01排列组合顺序)

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int M=11,Q=1<<10;
ll f[M][Q][M*M],ans;
int num[Q],s[Q],N,K,s0;
void pre(){//行内预处理,状压
    s0=0;ans=0; memset(f,0,sizeof(f));
    for(int i=0;i<(1<<N);i++){
        if(i&(i<<1))continue;
        int cnt=0;
        for(int j=0;j<N;j++)
            if(i&(1<<j))cnt++;
        ++s0;
        s[s0]=i;  //第s0个状态是i
        num[s0]=cnt;//第s0个状态可放置k个国王
    }
}
void dp(){
    int i,j,k,t;
    f[0][1][0]=1;//状压dp都是初始化为1

    for(i=1;i<=N;i++)//以行作为阶段
        for(j=1;j<=s0;j++)//枚举第i行状态
            for(k=num[j];k<=K;k++)//枚举前i行(包括第i行)能合法放置的国王数
                for(t=1;t<=s0;t++)//枚举第i-1行状态
                    if( !(s[t]&s[j])&&!(s[t]&(s[j]<<1))&&!(s[t]&(s[j]>>1)) )//无冲突
                        f[i][j][k]+=f[i-1][t][k-num[j]];//有背包的味道
    for(i=1;i<=s0;i++)ans+=f[N][i][K];//注意是K
    cout<<ans<<endl;
}
int main(){
    while(cin>>N>>K)pre(),dp();
    return 0;
}

例二:

炮兵阵地

要点:

  • 注意本题求在整个地图区域内最多能够摆放多少我军的炮兵部队,与例一(求情况数)不同
  • 难点: d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示第 i i i行为状态 j j j,第 i − 1 i - 1 i1行为状态为 k k k时所用的最大炮兵数
  • d p [ i ] [ j ] [ p ] = m a x ( d p [ i ] [ j ] [ p ] , d p [ i − 1 ] [ p ] [ q ] + n u m [ j ] ) dp[i][j][p] = max(dp[i][j][p], dp[i - 1][p][q] + num[j]) dp[i][j][p]=max(dp[i][j][p],dp[i1][p][q]+num[j])( q , p , j q,p,j qpj各自合理且之间均不发生冲突, n u m [ j ] num[j] num[j] j j j状态的炮兵数)
  • 为了节约空间,可以用滚动数组优化
  • 每一行的合法状态远不及1<<10这么多,事实上开到100足矣

优化前:

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
const int N=105,Q=100;
//dp[i][j][k]表示第i行状态为j和第i-1行状态为k时所用的最大炮兵数
int dp[N][Q][Q];
int state[Q],cnt=0,g[N],num[Q];
int n,m;
bool judge(int x){//炮兵十字左右相邻2格不能放的
    if(x&(x>>1)) return false;
    if(x&(x>>2)) return false;
    return true;
}
int count(int x){//计算1的个数
    int ans=0;
    for(int i=0;i<m;i++){
        if((x>>i)&1) ans++;
    }
    return ans;
}
int main(){
    int i,j,k,h;
    char fu;
    scanf("%d %d",&n,&m);
    //读入二进制棋盘,0可以放炮兵
    for(i=1;i<=n;i++)for(j=0;j<m;j++){
        scanf(" %c",&fu);//cin>>fu;注意区分
        g[i] += (fu=='H')<<j;
    }
    //行内预处理
    for(i=0;i<(1<<m);i++)
        if(judge(i)){
            state[++cnt]=i;
            num[cnt]=count(i);
        }
    for(i=1;i<=n;i++)
        for(j=1;j<=cnt;j++)//这行状态
            for(k=1;k<=cnt;k++)//上一行状态
                for(h=1;h<=cnt;h++){//上上行状态
                    int a=state[j],b=state[k],c=state[h];
                    if((a&b)|(b&c)|(a&c)) continue;//炮兵十字上下相邻2格不能放的
                    //理论上每一行都会被检查,所以只检查当前行就可以了,而无须检查第 i - 1 和 i - 2 行
                    if(g[i]&a) continue;//判断这行的状态跟原图实际状态是否相符(不一定一样)
                    dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][h]+num[j]);
                }
    int ans=0;
    for(i=1;i<=cnt;i++)
        for(j=1;j<=cnt;j++){
            ans=max(ans,dp[n][i][j]);
        }
    printf("%d\n",ans);
    return 0;
}

优化后:

                //之前的省略
                dp[i&1][j][k]=max(dp[i&1][j][k],dp[(i-1)&1][k][h]+num[j]); 
                }   
    int ans=0;
    for(i=1;i<=cnt;i++)
        for(j=1;j<=cnt;j++){
            ans=max(ans,dp[n&1][i][j]);
        }//n为奇数时n&1为1,反之为0  
    printf("%d\n",ans);
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值