状态压缩DP练习

本文通过实例展示了如何使用状态压缩动态规划方法解决四个不同场景的问题:小国王的布局策略、玉米田的种植策略、炮兵阵地的布局优化和愤怒小鸟的抛物线覆盖。通过状压DP,巧妙地处理了棋盘类和集合类问题,适用于动态调整状态的复杂问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

小国王

题目链接:小国王
分析:看到这题我们可能会想到N皇后问题,但是这道题用爆搜的话是会TLE的,这里我们考虑状压DP,只要我们能想出来表示如何每个表示每个状态,再考虑到细节,基本就能做出来。详细的看代码注释吧。
代码实现:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N=12,M=1<<10,K=110;//这里N开到12,后面会解释原因
LL dp[N][K][M];//dp[i][j][k]表示考虑前i行,已放置了j个国王,放置的状态为k的情况(k的某一位为1,就说明本行那一个位置有国王,注意位数从0开始)
vector<int> state;//存储所有合法状态
int cnt[M];//cnt[i]存储i的二进制表示中1的个数
vector<int> head[M];//head[i]存储i状态能转移到的状态,也是所有能转移到此状态的状态
int n,m;
bool check(int x){//检查一个数的二进制表示中是否有连续的两个1,若没有,返回true(因为若有两个连续的1就说明两个国王挨着,不合题意)
    return !(x&x>>1);
}
int count(int x){//返回x的二进制表示中1的个数
    int ans=0;
    for(int i=0; i<n; i++)
        ans+=(x>>i & 1);
    return ans;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<1<<n;i++){
        if(check(i)){
            state.push_back(i);//加入合法状态
            cnt[i]=count(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)){//a和b没有重叠的1并且没有相邻的1,因为国王攻击的范围是9宫格,所以上一行的国王不能与这一行的国王在列上相邻
                head[i].push_back(j);
            }
        }
    }
    dp[0][0][0]=1;//什么也没摆也是一种方案
    for(int i=1;i<=n+1;i++){//考虑n+1行
        for(int j=0;j<=m;j++){//考虑一共放置了0~m个国王
            for(int a=0;a<state.size();a++){//考虑本行的每个状态
                int c=cnt[state[a]];//求出此状态国王数
                if(j>=c){//如果j>=c才符合实际
                    for(auto x:head[a]){//遍历所有能转移到state[a]状态的状态
                        dp[i][j][a]+=dp[i-1][j-c][x];//加上方案数
                    }
                }
            }
        }
    }
    cout<<dp[n+1][m][0]<<endl;//前n+1行,并且摆了m个国王,第n+1行没摆的方案数就是答案(因为我们不确定第n行摆几个才是答案,不过累加起来也能算,这里比较懒了),这也是n开到12的原因
    return 0;
}

玉米田

题目链接:玉米田
分析:和上题差不多,但又没上题难,就直接上代码吧。解析详见注释。
代码实现:

#include<iostream>
#include<vector>
using namespace std;
const int N=15,M=1<<12,mod=1e8;
int dp[N][M];//dp[i][j]表示考虑前i行,第i行种植状态为j的方案数(j的某一位为1就说明这一位有玉米)
int n,m;
int g[N];//g数组描述每一行的状态,若第i位为1,则说明此处不能种
vector<int> state;
vector<int> head[M];
bool check(int x){//检查二进制表示中有无相邻的1
    return !(x&x>>1);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=0;j<m;j++){
            int c;
            scanf("%d",&c);
            g[i]+=(!c)<<j;//因为我习惯用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)){//如果a,b状态没有重合位置,即上下行玉米不相邻,则可以相互转移
                head[i].push_back(j);
            }
        }
    }
    dp[0][0]=1;//什么也不放也是一种方案
    for(int i=1;i<=n+1;i++){//和上题同理
        for(int j=0;j<state.size();j++){//遍历所有状态
            if(!(state[j]&g[i])){//如果此状态满足没有种到坏土地上,就满足题意
                for(auto k:head[j]){//加上方案数,记得取模
                    dp[i][j]=(dp[i-1][k]+dp[i][j])%mod;
                }
            }
        }
    }
    cout<<dp[n+1][0]<<endl;//和上题同理
    return 0;
}

炮兵阵地

题目链接:炮兵阵地
分析:其实和前面的题都差不多,不过这个题中一个炮兵会对后两行都造成影响,因此我们在DP时需要考虑到前两行的状态,其余的都差不多。
代码实现:
这里用了滚动数组优化。

#include<iostream>
#include<vector>
using namespace std;
const int N=110,M=1<<10;
int n,m;
int g[N];
int dp[2][M][M];//dp[i][j][k]表示考虑第i行,上一行状态是j,这一行状态是k的情况中摆放炮兵的最大值
vector<int> state;
int cnt[M];
bool check(int x){//这里也不能有二进制表示中两个1只隔一个0的情况
    return !(x&x>>1||x&x>>2);
}
int count(int x){//x二进制中1的个数
    int res=0;
    for(int i=0;i<m;i++){
        if(x>>i&1)  res++;
    }
    return res;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=0;j<m;j++){
            char c;
            scanf(" %c",&c);
            g[i]+=(c=='H')<<j;//还是用1表示不能放置的状态
        }
    }
    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;i++){//枚举每一行
        for(int j=0;j<state.size();j++){//枚举上一行
            for(int k=0;k<state.size();k++){//枚举这一行
                for(int u=0;u<state.size();u++){//枚举上上一行
                    int a=state[j],b=state[k],c=state[u];
                    if(a&b | b&c | c&a) continue;//任意两行都不能有在人同一列的情况
                    if(g[i]&b|g[i-1]&a) continue;//这一行和上一行都不能放在H上
                    dp[i&1][j][k]=max(dp[i&1][j][k],dp[i-1&1][u][j]+cnt[b]);//转移状态
                }
            }
        }
    }   
    int res = 0;
    for (int i = 0; i < state.size(); i ++ )//找出最终答案
        for (int j = 0; j < state.size(); j ++ )
            res = max(res, dp[n & 1][i][j]);
    //优化:直接循环n+2行,输出dp[n+2&1][0][0];
    cout << res << endl;
    return 0;
}

愤怒的小鸟

题目链接:愤怒的小鸟
分析:前面的题都是棋盘类的状压DP,基本还是有章可循的,而这个集合类的感觉真是太难了。在本题中,由于一定过原点,抛物线方程可设为y=ax^2+bx,所以两点就能确定一个抛物线,我们可以处理出来n^2个抛物线,然后分别求出每个抛物线覆盖的点集,标准做法是Dancing Links,这里n的范围较小,可以直接状压DP,代码更简单。我们设f[i]表示覆盖点集的状态是i时所需的最少抛物线数,那么状态转移的时候,我们只需找到没被覆盖的一个点,枚举所有包含这个点的抛物线即可。
代码实现:
细节还是比较多的。

#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
int T;
int n,m;
typedef pair<double,double> PDD;
const int N=18,M=1<<18;
const double eps=1e-6;
int dp[M];
int path[N][N];//path[i][j]表示i和j两点确定的抛物线能消灭小猪的二进制表示
PDD pos[N];//每只猪的位置
bool cmp(double x,double y){//比较两个浮点数是否相同,相同则返回1
    if(fabs(x-y)<eps)   return 1;
    else return 0;
}
int main(){
    scanf("%d",&T);
    while(T--){
        memset(dp,0x3f,sizeof dp);//首先初始化为很大的数
        memset(path,0,sizeof path);
        dp[0]=0;//dp[0]表示消灭0只猪,肯定不需要小鸟
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++){
            scanf("%lf%lf",&pos[i].first,&pos[i].second);
        }
        for(int i=0;i<n;i++){
            path[i][i]=1<<i;//只考虑这一个点的时候只能除去它自己
            for(int j=0;j<n;j++){
                double x1=pos[i].first,y1=pos[i].second,x2=pos[j].first,y2=pos[j].second;
                if(cmp(x1,x2))  continue;//如果横坐标相同,则此两点组成不了抛物线
                double a=(y1/x1-y2/x2)/(x1-x2);//求抛物线的系数a和b
                double b=y1/x1-a*x1;
                if(cmp(a,0.0)||a>0) continue;//注意a必须小于0
                int tmp=0;
                for(int k=0;k<n;k++){//处理出来这个抛物线能消灭的小猪的二进制表示
                    double x=pos[k].first,y=pos[k].second;
                    if(cmp(a*x*x+b*x,y)) tmp+=1<<k;
                }
                path[i][j]=tmp;
            }
        }
        for(int i=0;i<1<<n;i++){//枚举已经消灭小猪的所有状态
            int x=0;
            for(int j=0;j<n;j++){//找出需要消灭的第一只小猪,记作x
                if(!(i>>j&1)){
                    x=j;
                    break;
                }
            }
            for(int j=0;j<n;j++){//找出消灭x的所有抛物线来转移状态
                dp[i|path[j][x]]=min(dp[i|path[x][j]],dp[i]+1);
            }
        }
        cout<<dp[(1<<n)-1]<<endl;//输出答案
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_bxzzy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值