状态压缩模型与题目详解acm_t


#写在前面

状态压缩分为两大类

  1. 棋盘式(基于连通性)
  2. 集合

#递归实现指数型枚举

https://www.acwing.com/problem/content/94/
状态压缩就是用一个二进制数来表示某一位上的东西取还是不取
9-------1001
表示第一位取,第2、3位不取,第4位取
当我们从 1----0001 枚举到 15----1111
会发现所有的取法都被遍历到了

----c++版

#include<iostream>
using namespace std;

int n;
void dfs(int u, int state){
    if(u==n){
        for(int i=0;i<n;i++)//将这个状态,的二进制数,如010,按照1不用,用2,3不用打印出来
            if(state>>i&1)//看看二进制第i位是不是1,是就打印出来
                cout<<i+1<<' ';//由于我们从第0位开始看的,所以要加一
        cout<<endl;
        return;
    }
    dfs(u+1, state);//不使用第u位的数字(毕竟一开始默认是0)
    dfs(u+1, state|1<<u);//将第u位设置为1,表示使用第u位的数字
}

int main(){
    cin>>n;
    dfs(0,0);
    return 0;
}

#棋盘式

##蒙德里安的梦想

https://www.acwing.com/problem/content/293/

----c++版

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//经典状态压缩
//摆完所有横向的1x2纸块,纵向的纸块只有1种摆法,所以只考虑横向先
//f[i,j]集合:第i列,j存储上一列有哪些横着摆的纸挡住了下一列的空格。
//            j是个二进制数,0表示没挡住,1表示挡住了
//属性:个数
//计算:f[i,j]+=f[i-1,k];
const int N=12, M=1<<N;
int n,m;
long long f[N][M];
bool st[M];

int main(){
    while(cin>>n>>m, n||m){
        memset(f, 0, sizeof f);
        //对于每一列,其各种合法的状态集合都是一样的,
        //合法指列里余下的空格可以摆放竖着的1x2纸片
        //所以可以预处理出来,用st来存合法的状态
        for(int i=0;i<1<<n;i++){//枚举 列 可能的所有状态
            st[i]=true;
            int cnt=0;
            for(int j=0;j<n;j++)//枚举列的每一行
                if(i>>j&1){//如果当前是1,看看连续的0是否有奇数个
                    if(cnt&1)st[i]=false;//奇数个0的状态是不合法的
                    cnt=0;
                }else cnt++;
            if(cnt&1)st[i]=false;
        }

        f[0][0]=1;//第0列只能竖着放,只有一种方案
        for(int i=1;i<=m;i++)//枚举每一列
            for(int j=0;j<1<<n;j++)//枚举列可能的所有状态
                for(int k=0;k<1<<n;k++)//枚举上一列可能的所有状态
                    if((j&k)==0&&st[j|k])//j与k不能有冲突,且该状态合法
                        f[i][j]+=f[i-1][k];//转移
        cout<<f[m][0]<<endl;
    }
    return 0;
}

##小国王

https://www.acwing.com/problem/content/1066/
在这里插入图片描述

----c++版

#include<iostream>
#include<algorithm>
using namespace std;
//当我们在模拟放国王的时候,我们会发现:
//一行一行放置时,每一行的状态只与上一行的状态有关
//上上一行没有影响了

//集合:f[i,j,s]:前i行已经放完了,并且当前已经放了j个棋子,且第i行状态是s的方案集合
//属性;数量
//计算:
#include<cstring>
#include<vector>
const int N=12,M=1<<10,K=110;

typedef long long ll;

int n,m;
vector<int> state;
int id[M], cnt[M];//cnt每个状态里1的个数
vector<int> head[M];//每一个状态可以转移到的其他状态
ll f[N][K][M];

bool check(int state){
    for(int i=0;i<n;i++)
        if((state>>i&1)&&(state>>i+1&1))//不能有相邻的1,就是一行的一个合法的状态
            return false;
    return true;
}

int count(int state){
    int res=0;
    for(int i=0;i<n;i++)res+=state>>i&1;
    return res;
}

int main(){
    cin>>n>>m;
    //一般来说最好先算一下
    for(int i=0;i<1<<n;i++)
        if(check(i)){
            state.push_back(i);
            id[i]=state.size()-1;//id存合法状态i的下标是多少
            cnt[i]=count(i);//cnt存状态i里有几个1
        }
    
    //预处理一下哪些状态可以相互转移
    for(int i=0;i<state.size();i++)
        for(int j=0;j<state.size();j++){
            int a=state[i],b=state[j];
            if((a&b)==0&& check(a|b))//国王不在同一列上,且两个状态的并不能有连续的1
                head[i].push_back(j);
        }
    
    //dp
    f[0][0][0]=1;
    for(int i=1; i <= n+1 ;i++)//+1省去了最后的枚举,所以一开始开到了12
        for(int j=0;j<=m;j++)
            for(int a=0;a<state.size();a++)
                for(int b: head[a]){//这里a、b是状态的下标
                    int c=cnt[state[a]];
                    if(j>=c){
                        f[i][j][a]+=f[i-1][j-c][b];
                    }
                }
    cout<<f[n+1][m][0]<<endl;
    return 0;
}

##玉米田

https://www.acwing.com/problem/content/329/
在这里插入图片描述

----c++版

#include<iostream>
#include<algorithm>
using namespace std;
//也是只与上一行的状态有关
#include<vector>
const int N=14,M=1<<12,mod=1e8;
int n,m;
vector<int> state;
int g[N];
vector<int> head[M];
int f[N][M];

bool check(int state){
    for(int i=0;i<m;i++)
        if((state>>i&1)&&(state>>i+1&1))//不能有相邻的1
            return false;
    return true;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=0;j<m;j++){
            int t;
            cin>>t;//二进制存地图
            g[i]+=!t<<j;//是0的地方设置成1,方便做与运算
        }
    
    //预处理状态
    for(int i=0;i<1<<m;i++)
        if(check(i))
            state.push_back(i);
    
    //预处理能够互相转移的状态
    for(int i=0; i<state.size();i++)
        for(int j=0;j<state.size();j++){
            int a=state[i],b=state[j];
            if((a&b)==0)
                head[i].push_back(j);
        }
    
    f[0][0]=1;
    for(int i=1;i<=n+1;i++)
        for(int a=0;a<state.size();a++)
            for(int b:head[a]){
                if(g[i]&state[a])continue;
                f[i][a] = (f[i][a]+f[i-1][b])%mod;
            }
    
    cout<<f[n+1][0]<<endl;
    return 0;
}

##炮兵阵地

https://www.acwing.com/problem/content/294/
在这里插入图片描述

----c++版

#include<iostream>
#include<algorithm>
using namespace std;
//由于空间限制比较小,所以要用滚动数组
#include<cstring>
#include<vector>
const int N=11,M=1<<10;
int n,m;
int g[110];
vector<int> state;
int f[2][M][M];//不然要开一个亿的数组
int cnt[N];

bool check(int state){
    for(int i=0;i<m;i++)
        if((state>>i&1)&&(state>>i+1&1)|((state>>i+2&1)))
            return false;
    return true;
}

int count(int state){
    int res=0;
    for(int i=0;i<m;i++)res+=state>>i&1;
    return res;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=0;j<m;j++){
            char c;
            cin>>c;
            if(c=='H')g[i]+=1<<j;
        }
    
    for(int i=0;i<1<<m;i++)
        if(check(i)){
            state.push_back(i);
            cnt[i]=count(i);
        }
        
    for(int i=1;i<=n+2;i++)
        for(int j=0;j<state.size();j++)//第i-1行的状态
            for(int k=0;k<state.size();k++)//第i行的状态
                for(int u=0;u<state.size();u++){//倒数第二行的状态
                    int a=state[j], b=state[k], c=state[u];
                    if((a&b)|(b&c)|(a&c))continue;
                    if(g[i-1]&a|g[i]&b)continue;
                    f[i&1][j][k]=max(f[i&1][j][k], f[i-1&1][u][j]+cnt[b]); //注意与1,滚动数组   
                }
    
    cout<<f[n+2&1][0][0]<<endl;
    
    return 0;
}

#集合式

##最短hamilton路径

https://www.acwing.com/problem/content/93/

----c++版

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//集合:f[i,j]所有从0走到j,走过的所有点是i的所有路径
//      i是个压缩状态,存一个二进制数,表示点是否走过
//属性:min
//计算:用倒数第二个点来分类, 保证从0到倒数第二个点的不含倒数第一个点的所有路线中取最短
const int N=20,M=1<<N;
int n;
int w[N][N];
int f[M][N];

int main(){
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>w[i][j];
    memset(f, 0x3f, sizeof f);
    f[1][0]=0;
    for(int i=0;i<1<<n;i++)
        for(int j=0;j<n;j++)
            if(i>>j&1)//从0走到j,i中必须包含j
                for(int k=0;k<n;k++)
                    if((i-(1<<j))>>k&1)//除去j后,i中还得有第k个点才能保证状态的转移
                        f[i][j]=min(f[i][j], f[i-(1<<j)][k]+w[k][j]);
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}

##愤怒的小鸟

https://www.acwing.com/problem/content/526/

爆搜的方式

在这里插入图片描述
在这里插入图片描述

----c++版

#include<iostream>
#include<algorithm>
using namespace std;
//经典的重复覆盖问题
//只需要两个点,即可确定此抛物线
//预处理出所有抛物线,最多n^2条
//并预处理出每条抛物线能够覆盖哪些点
//
//给定很多抛物线,最少选几条能覆盖所有点
//重复覆盖模型
//dancing links是最优解
//这里用状态压缩
#include<cstring>
#define x first
#define y second
const int N=18,M=1<<18;
typedef pair<double, double> pdd;

int n,m;
pdd q[N];
int path[N][N];
int f[M];

//注意浮点数比较
#include<cmath>
const double esp=1e-8;
int cmp(double x, double y){
    if(fabs(x-y)<esp)return 0;
    if(x<y) return -1;
    return 1;
}

int main(){
    int T;
    cin>>T;
    while(T--){
        cin>>n>>m;
        for(int i=0;i<n;i++)cin>>q[i].x>>q[i].y;
        
        //利用现有的所有点,找出所有穿过两个点的抛物线
        memset(path, 0, sizeof path);//path表示穿过i号点,j号点的抛物线能够覆盖哪些点
        for(int i=0;i<n;i++){
            path[i][i]=1<<i;//肯定能覆盖i号点
            for(int j=0;j<n;j++){
                double x1=q[i].x, y1=q[i].y;
                double x2=q[j].x, y2=q[j].y;
                if(!cmp(x1,x2))continue;//两个点不能在同一列上
                double a=(y1/x1-y2/x2)/(x1-x2);
                double b=y1/x1-a*x1;
                if(cmp(a,0)>=0)continue;
                
                int state=0;
                //哪些点会被当前抛物线覆盖
                for(int k=0;k<n;k++){
                    double x=q[k].x, y=q[k].y;
                    if(!cmp(a*x*x+b*x, y))state+=1<<k;
                }
                path[i][j]=state;//state:穿过i号点和j号点的抛物线能覆盖的点的状态
            }
        }
        
        memset(f, 0x3f, sizeof f);
        f[0]=0;//0这个状态不需要抛物线
        for(int i=0;i+1<1<<n;i++){//枚举所有状态   +1是因为当我们包含所有列就不用更新了
            int x=0;//x找没有被覆盖掉的列(没有被覆盖的点)
            for(int j=0;j<n;j++)
                if(!(i>>j&1)){//如果二进制第i-j位上是0
                    x=j;
                    break;
                }
            //枚举所有能覆盖x的抛物线      dp
            for(int j=0;j<n;j++)
                f[i|path[x][j]]=min(f[i|path[x][j]], f[i]+1);
        }
        cout<<f[(1<<n)-1]<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值