状态压缩习题

洛谷P2704 [NOI2001] 炮兵阵地

题目链接
因为列数仅为10,考虑使用状态压缩,用一个数字 s s s表示某行的炮兵放置情况,当 s s s二进制的 k k k位为1,该行的 k k k列就放置了炮兵。
预处理: 因为要计算炮兵的数量,先计算每个状态 s s s的炮兵个数(即二进制中 1 1 1的数量) s u m [ s ] sum[s] sum[s]

状态设计: d p [ i ] [ s 1 ] [ s 2 ] dp[i][s1][s2] dp[i][s1][s2]表示前 i i i行已经安排好,第 i − 1 i-1 i1行的状态为 s 1 s1 s1,第 i i i行的状态为 s 1 s1 s1,可以放置的最多炮兵数量。起始状态是第一行所有合法状态。
状态转移: 其实状态转移比较简单,比较冗杂的是状态转移的判断。分三种情况:避免当前行的炮兵放置在山地上;避免当前行的炮兵相互攻击;如果有上一行(上上行),需要避免当前行和上一行(上上行)的炮兵相互攻击。如果都合法就增加当前行安排的状态的炮兵数量。

空间优化: 需要安排好前 i − 1 i-1 i1行才能开始安排第 i i i行,也就是说第 i i i行的状态只和前 i − 1 i-1 i1行有关,因此保存第前一行和当前行的状态就够了,第一维只需要2的长度保存当前行和前一行的状态。

#include<bits/stdc++.h>
using namespace std;
int dp[2][1<<10][1<<10],a[110],sum[1<<10],n,m;
inline int get_sum(int x){
    int ans=0;
    while(x){
        ans+=(x&1);
        x>>=1;
    }return ans;
}
inline bool check(int x,int i){
    if((x&i)||(x&(x<<1))||(x&(x<<2)))return false;
    return true;
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            char ch;cin>>ch;
            a[i]<<=1;
            a[i]+=(ch=='H');
        }
    }
    for(int i=0;i< 1<<m;i++)
        sum[i]=get_sum(i);

    //初始化第一行
    for(int i=0;i< 1<<m;i++){
        if(check(i,a[0]))
            dp[0][0][i]=sum[i];
    }

    //初始化第二行
    for(int s1=0;s1< 1<<m;s1++){
        if(!check(s1,a[0]))continue;
        for(int s2=0;s2< 1<<m;s2++){
            if(check(s2,a[1])&&!(s1&s2))
                dp[1][s1][s2]=dp[0][0][s1]+sum[s2];
        }
    }

    //更新后n-2行
    for(int i=2;i<n;i++){
        for(int s1=0;s1< 1<<m;s1++){
            if(!check(s1,a[i-1]))continue;
            for(int s2=0;s2< 1<<m;s2++){
                if(!check(s2,a[i])||(s1&s2))continue;
                for(int s0=0;s0< 1<<m;s0++){
                    if(!check(s0,a[i-2])||(s1&s0)||(s2&s0))continue;
                    dp[i%2][s1][s2]=max(dp[i%2][s1][s2],dp[(i-1)%2][s0][s1]+sum[s2]);
                }
            }
        }
    }
    int ans=0;
    for(int i=0;i< 1<<m;i++)
        for(int j=0;j< 1<<m;j++)
            ans=max(ans,dp[(n-1)%2][i][j]);
    cout<<ans<<endl;
    return 0;
}

洛谷 P1879 [USACO06NOV]Corn Fields G

题目链接
状态设计: d p [ i ] [ s ] dp[i][s] dp[i][s]表示前 i i i行已经安排好,第 i i i行的状态为 s s s的方案数。
状态转移: 需要进行三种判断:避免当前状态本身冲突,因为需要养一只🐂空一只🐂,因此 ( s & ( s < < 1 ) ) (s\&(s<<1)) (s&(s<<1)) 为0;避免把牛养在贫瘠的土地上,输入时将0和1颠倒, a [ i ] a[i] a[i]二进制的 j j j位为1,表示位置 ( i , j ) (i,j) (i,j)为贫瘠土地不适合养牛,因此 ( s & a [ i ] ) (s\&a[i]) (s&a[i])为0;最后还要避免前一行和当前行同列都养牛。

#include<bits/stdc++.h>
using namespace std;
const int N=12,mod=1e9;
int dp[N][1<<N],a[N];
int n,m,cnt;
inline bool check(int x,int i){
    if((x&(x<<1))||(x&i))return false;
    return true;
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            int x;cin>>x;
            a[i]<<=1;
            a[i]+=(x==0);//a[i]二进制位为1表示不能种艹
        }
    }
    //初始化第一行
    for(int i=0;i< 1<<m;i++){
        if(check(i,a[0]))dp[0][i]=1;
    }
    //更新后n-1行
    for(int i=1;i<n;i++){
        for(int s1=0;s1< 1<<m;s1++){
            if(!check(s1,a[i-1]))continue;
            for(int s2=0;s2< 1<<m;s2++){
                if((s1&s2)||!check(s2,a[i]))continue;
                dp[i][s2]=(dp[i-1][s1]+dp[i][s2])%mod;
            }
        }
    }
    int ans=0;
    for(int i=0;i< 1<<m;i++)
        ans=(ans+dp[n-1][i])%mod;
    cout<<ans<<endl;
    return 0;
}

洛谷 P1896 [SCOI2005]互不侵犯

题目链接
预处理: 因为要枚举的内容很多,我们先预处理出合法的状态数 s s s,因为此题涉及到国王个数,顺便预处理每个状态包含国王数量 s u m sum sum
状态设计和转移: d p [ i ] [ j ] [ m ] dp[i][j][m] dp[i][j][m]表示安排完前 i − 1 i-1 i1行,第 i i i行的状态为 s [ j ] s[j] s[j],且当前安排完的国王数量已经达到 m m m的方案数。然后判断会不会和前一行的枚举状态 s [ l ] s[l] s[l]冲突,若不冲突直接加上 d p [ i − 1 ] [ l ] [ m − s u m [ j ] dp[i-1][l][m-sum[j] dp[i1][l][msum[j]

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=10;
LL dp[N][1<<N][N*N];
int sum[1<<N],s[1<<N];
int cnt,n,k;
//状态设计: dp[i][s][m]表示安排完前i行,第i行的状态为s,且当前安排完的国王数量已经达到m的方案数。
int main(){
    cin>>n>>k;
    for(int i=0;i< 1<<n;i++){
        if(i&(i<<1))continue;
        s[++cnt]=i;
        int cur=i;
        while(cur){
            if(cur&1)sum[cnt]++;
            cur>>=1;
        }
    }
    for(int i=1;i<=cnt;i++)dp[1][i][sum[i]]=1;
    for(int i=2;i<=n;i++){//枚举当前行数
        for(int j=1;j<=cnt;j++){//枚举当前一行的状态
            for(int m=0;m<=k;m++){//枚举到达当前行总共有多少个国王
                if(m<sum[j])continue;
                for(int l=1;l<=cnt;l++){//枚举上一行的状态
                    if((s[l]&s[j])||(s[l]&(s[j]<<1))||(s[l]&(s[j]>>1)))continue;
                    dp[i][j][m]+=dp[i-1][l][m-sum[j]];
                }
            }
        }
    }
    LL ans=0;
    for(int i=1;i<=cnt;i++)
        ans+=dp[n][i][k];
    cout<<ans<<endl;
    return 0;
}

洛谷 P3092 [USACO13NOV]No Change G

题目链接
参考题解
状态设计: 因为硬币只有16个,因此可以用 s t st st表示使用硬币的情况,数字 s t st st的二进制 j j j位置为0表示第 j j j个硬币不使用,若其为1表示当前状态使用第 j j j个硬币。 d p [ s t ] dp[st] dp[st]表示当前硬币使用情况为 s t st st时,从左到右购买物品最多可以购买到哪一个物品。仍然使用刷表法,即从当前状态可以向哪些状态转移。假设当前状态为 s t 0 st0 st0,可以转移到 s t st st,那应该满足的条件就是 s t 0 & ( 2 j ) = 0 st0 \& (2^j)=0 st0&(2j)=0,同时 s t = s t 0 + ( 2 j ) st=st0+(2^j) st=st0+(2j)。表示 s t 0 st0 st0这个取硬币的状态再加上第 j j j个硬币就变成 s t st st
状态转移: 再考虑 d p [ s t 0 ] dp[st0] dp[st0]如何转移到 d p [ s t ] dp[st] dp[st]。因为多加了第 j j j个硬币,那就用第 j j j个硬币去购买从 d p [ s t 0 ] + 1 dp[st0]+1 dp[st0]+1个物品开始以右的最多物品,因为从第一个物品到第 d p [ s t 0 ] dp[st0] dp[st0]个都已经被购买了。朴素做法是从第 d p [ s t 0 ] + 1 dp[st0]+1 dp[st0]+1开始向右边枚举求和达到第 j j j个硬币的价值。此处可以用前缀和与二分作为优化。二分可以达到的最右边的物品 R R R,使得 ∑ i = d p [ s t 0 ] + 1 i = R v a l i \sum_{i=dp[st0]+1}^{i=R}{val_i} i=dp[st0]+1i=Rvali不超过第 j j j个硬币的价值。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e5+7,K=1<<16;
int dp[K],coin[16],n,k;
LL sum[N];
inline LL cal(int s){//还没有使用的硬币就是剩下的钱
    LL ans=0;
    for(int i=0;i<k;i++){
        if(s&(1<<i))continue;
        ans+=coin[i];
    }return ans;
}
int main(){
    cin>>k>>n;
    for(int i=0;i<k;i++)cin>>coin[i];
    for(int i=1;i<=n;i++){
        int x;cin>>x;
        sum[i]=sum[i-1]+x;
    }
    LL ans=-1;
    for(int s0=0;s0< 1<<k;s0++){
        for(int j=0;j<k;j++){
            if(s0&(1<<j))continue;
            int s=s0|(1<<j);

            //二分查找j硬币可以购买到最右边的货物
            int l=dp[s0]+1,r=n,mid,res;
            while(l<=r){
                mid=(l+r)>>1;
                if(sum[mid]-sum[dp[s0]]>coin[j]){
                    r=mid-1;
                }
                else {
                    l=mid+1;
                    res=mid;
                }
            }
            //因为当前的s可能从别的s0转移过来,如果当前s0转移过来达到的最右端反而更小,就应该保存之前的结果
            if(dp[s]>res)continue;
            dp[s]=res;
            //如果可以买完全部货物就更新答案
            if(dp[s]==n)ans=max(cal(s),ans);
        }
    }
    cout<<ans<<endl;
    return 0;
}

洛谷P3694 邦邦的大合唱站队

题目链接
状态设计: 团队数量很少仅为20,考虑使用状态压缩,设 d p [ s ] dp[s] dp[s]为状态为 s s s的队形需要出队的最少人数。状态 s s s的含义是:若 s s s j j j个位置为1,表示第 j j j个团队的人都已经靠在一起。

预处理状态:
s u m [ i ] [ j ] sum[i][j] sum[i][j]:前 i i i个人中属于团队 j j j的人数;
l e n [ s ] len[s] len[s]状态 s s s中已经排列好的人数;
n u m [ j ] num[j] num[j]:团队 j j j的总人数。

状态转移: d p [ s ] dp[s] dp[s]转移向 d p [ s 1 ] dp[s1] dp[s1]:排完 s s s中的团队(状态s不包含第 j j j个队伍), s 1 = s    ^ ( 1 < < j ) s1=s\hat{\space\space}(1<<j) s1=s  ^(1<<j),表示接下来将第 j j j个团队,排列在 s s s状态中已经排列好的团队之后。具体过程如下图:
在这里插入图片描述
其实最朴素的想法就是对团队编号进行全排列,然后计算不同排列需要出队的人数取最小值;但是全排列肯定TLE,我们把排列拆分为一层层看,第1层确定第1个位置分配给哪个团队,第2层是在确定了第1个位置分配给哪个团队后再枚举第2个位置分配给的团队…给排列那棵树多了很多剪枝,假设第 1 , 3 , 4 1,3,4 1,3,4个团队排在前面,已经知道排列 143 143 143可以使出队人数最少,那我们每次用它们继续扩展下一层时,就少了 134 , 314 , 341 , 413 , 431 134,314,341,413,431 134,314,341,413,431这几种排列的拓展。从而将原本 O ( k ! ) O(k!) O(k!)的复杂度降低为 O ( 2 k ) O(2^k) O(2k)

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e5+7,M=1<<20;
int dp[M],n,m,sum[N][20],num[20],len[M];
int main(){
    cin>>n>>m;
    memset(dp,0x3f,sizeof dp);
    
    for(int i=1;i<=n;i++){
        int x;cin>>x;
        x--;num[x]++;
        for(int j=0;j<m;j++){
            sum[i][j]=sum[i-1][j];
        }sum[i][x]++;
    }
    
    //预处理每个状态i已经排好的总人数
    for(int i=0;i< 1<<m;i++){
        for(int j=0;j<m;j++){
            if(i&(1<<j))len[i]+=num[j];
        }
    }
    
    //起始状态是一个团队都没有排列 
    dp[0]=0;
    
    for(int s=0;s< 1<<m;s++){
        for(int j=0;j<m;j++){
            if(s&(1<<j))continue;
            int s1=s^(1<<j);
            dp[s1]=min(dp[s1],dp[s]+num[j]-(sum[len[s1]][j]-sum[len[s]][j]));
        }
    }
    cout<<dp[(1<<m)-1]<<endl;
    return 0;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值