嗯,作为一只蒟蒻,今天再次学习了状压dp(学习借鉴的博客)
但是,依旧懵逼··································
这篇学习笔记是我个人对于状压dp的理解,如果有什么不对的地方,希望大家指出。
闲话不多说,进入正题。
首先,在介绍状压dp之前,我们先来了解一下状态压缩(常用的为二进制,why?【因为其他的我不会】)。
什么是状态压缩呢?顾名思义,就是将数转换为二进制来进行一些操作。
基本操作:
看完基本操作,我们来看一下一些稍微复杂的操作。
操作 | 运算 |
取出整数n在二进制表示下的第k位 | (n>>k)&1 |
取出整数n在二进制表示下的第0~k-1位(后k位) | n&((1<<k)-1) |
取出整数n在二进制表示下的第k位取反 | n^(1<<k) |
取出整数n在二进制表示下的第k位赋值为1 | n|(1<<k) |
取出整数n在二进制表示下的第k位赋值为0 | n&(~(1<<k)) |
大概就是这些了,如果有看不懂的地方大家可以手推一下。
下面我们来看一道状态压缩的题
题目描述
现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。
现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。
输入输出格式
输入格式:
前两行两个数,n m
接下来m行,每行n个数,a[i][j]表示第i个开关对第j个灯的效果。
输出格式:
一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出-1
输入输出样例
3
2
1 0 1
-1 1 0
2
说明
对于20%数据,输出无解可以得分。
对于20%数据,n<=5
对于20%数据,m<=20
上面的数据点可能会重叠。
对于100%数据 n<=10,m<=100
题目分析:
我们刚拿到这道题时,可能会没有思路(就像我一样)
这时候,如果我们在考试的时候实在没有思路的话就……输出-1吧!
好了,不闹了,回归正题。看到这个题的时候我们会想到搜索,这并非不是一个可能的解法。
再看一下题目结构,只有开和关的两种状态,所以我们就可以用0和1来表示,此时我们就可以用二进制来表示。
开——1,关——0
接下来,我们就要进行搜索了。
由于在初始时我们得到的状态全部都是开的,所以我们就需要n个1,这时我们就用 int num=(<<(n))-; 来表示n个1。
然后我们在搜索时去寻找开关为1/-1的状态,并在找到时进行判断和步数的统计,最后判断是否将所有状态全部改正即可。
代码:
#include<bits/stdc++.h>
using namespace std;
template<typename type>
void scan(type &x){
type f=;x=;char s=getchar();
while(s<''||s>''){if(s=='-')f=-;s=getchar();}
while(s>=''&&s<=''){x=x*+s-'';s=getchar();}
x*=f;
}
const int maxn=;
struct node{
int step,zt;
};
int vis[maxn],mp[maxn][maxn];
int n,m; void bfs(int num){
queue<node>q;
node f;f.step=;f.zt=num;//初始状态
q.push(f);//入队
while(!q.empty()){
node u=q.front();
q.pop();
int pre=u.zt;
for(int i=;i<=m;i++){//枚举
int now=pre;
for(int j=;j<=n;j++){
if(mp[i][j]==){
if((<<(j-))&now){//如果第j位的开关为1,且当前的灯还开着,
now=now^(<<(j-));// 这时候将其状态修改
}}
else{ if(mp[i][j]==-){//如果第j位的开关为-1,且当前的灯关着,
now=((<<(j-))|now);// 这时候将其状态修改
}
}
}
f.zt=now;f.step=u.step+;
if(vis[now]==)continue;//打标记,防止重复
if(f.zt==){
vis[]=;//标记
printf("%d\n",f.step);
return ;
}
q.push(f);//入队
vis[now]=; //标记
} }
} int main(){
scan(n);scan(m);
int num=(<<(n))-;//找到n位1
for(int i=;i<=m;i++){
for(int j=;j<=n;j++){
scan(mp[i][j]);
}
}
bfs(num);
if(vis[]==){
printf("-1\n");
}
return ;
}
这样就可以了。
下面,我们来介绍一道十分基础的状压dp的题目
题目描述
Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can't be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.
Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.
农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
输入输出格式
输入格式:
第一行:两个整数M和N,用空格隔开。
第2到第M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。第i+1行描述了第i行的土地,所有整数均为0或1,是1的话,表示这块土地足够肥沃,0则表示这块土地不适合种草。
输出格式:
一个整数,即牧场分配总方案数除以100,000,000的余数。
输入输出样例
2 3
1 1 1
0 1 0
9
首先,我们在经历了第一题的练习,我们可以发现一些规律,如果题目给的数据范围并不是很大而且状态数量很多且都可以用0/1来表示,这时我们就可以去考虑去使用状态压缩。
所以我们就来考虑状压dp的方法,用以为数组来表示状态
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int M=1e6+,N=,mod=1e8;
int n,m,a[N][N],b[N],dp[N][M];
void in(){
scanf("%d%d",&n,&m);
for(int i=;i<=n;i++){
for(int j=;j<=m;j++){
scanf("%d",&a[i][j]);
}
}
}//读入
void update(){
for(int i=;i<=n;i++){
for(int j=;j<=m;j++){
b[i]=(b[i]<<)^a[i][j]^;//求取反码,判断是否为贫瘠土地,如果为1,则该土地贫瘠
}
}
}//求取贫瘠点
void judge(){
for(int i=;i<=(<<m)-;i++){
if(!(i&b[])&&!(i&(i<<))){
dp[][i]=;//第一行初始更新,满足条件
}
}
for(int i=;i<=n;i++){
for(int j=;j<=(<<m)-;j++){
for(int h=;h<=(<<m)-;h++){//枚举多次状态
if(h&j){
continue;//如果和上一行有交集,舍弃
}
if(h&(h>>)){
continue;//如果自己有相邻的,舍弃
}
if(h&(h<<)){
continue;//如果自己有相邻的,舍弃
}
if(h&b[i]){
continue;//如果有贫瘠的,舍弃
}
dp[i][h]+=dp[i-][j];
dp[i][h]=dp[i][h]%mod;//更新答案
}
}
}
ll ans=;
for(int j=;j<=(<<m)-;j++){
ans+=dp[n][j];
ans=ans%mod;
}
printf("ll%d",&ans);
}
int main(){
in();
update();
judge();
}
P1896
题目描述
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。
注:数据有加强(2018/4/25)
输入输出格式
输入格式:
只有一行,包含两个数N,K ( 1 <=N <=9, 0 <= K <= N * N)
输出格式:
所得的方案数
输入输出样例
3 2
16
读完题后,由于数据范围很小,但是状态数有很多,所以,我们就考虑来用状压dp。 代码:
#include<bits/stdc++.h>
using namespace std;
template <typename type>
void scan(type &x){
type f=;x=;char s=getchar();
while(s<''||s>''){if(s=='-')f=-;s=getchar();}
while(s>=''&&s<=''){x=x*+s-'';s=getchar();}
x*=f;
}
#define ll long long
int n,m;
ll dp[][][],ki[],st[],ans,sum; void init(){
int cnt=(<<n)-;//在最多的情况下,时每一位都有国王
for(int i=;i<=cnt;i++){
if(!((i<<)&i)){//如果两国王不相邻,则符合状态
st[++ans]=i;
int t=i;
while(t){
ki[ans]+=t%;//统计
t/=;
}
}
}
} int main(){
scan(n);scan(m);
init();
for(int i=;i<=ans;i++){//先处理第一行
if(ki[i]<=m)//只有第一行的国王总数不超过国王总数才可行
dp[][i][ki[i]]=;
}
for(int i=;i<=n;i++){//从第二行进行状态转移
for(int j=;j<=ans;j++){//这里和下一遍的枚举为的是处理 当前行和上一行的状态,来判断是否符合条件
for(int k=;k<=ans;k++){
if(st[j]&st[k])continue;//上下
if(st[j]&(st[k]<<))continue;//右上
if((st[j]<<)&st[k])continue;//左上
for(int l=;l<=m;l++){
if(ki[j]+l>m)continue;//如果超出了范围,舍去
dp[i][j][ki[j]+l]+=dp[i-][k][l];//否则进行转移
}
}
}
}
for(int i=;i<=n;i++){
for(int j=;j<=ans;j++){
sum+=dp[i][j][m];//统计答案
}
}
printf("%lld\n",sum);
return ;
}
2019.4.27更新
题目描述
这次小可可想解决的难题和中国象棋有关,在一个N行M列的棋盘上,让你放若干个炮(可以是0个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!
输入输出格式
输入格式:
一行包含两个整数N,M,之间由一个空格隔开。
输出格式:
总共的方案数,由于该值可能很大,只需给出方案数模9999973的结果。
输入输出样例
1 3
7
说明
样例说明
除了3个格子里都塞满了炮以外,其它方案都是可行的,所以一共有2*2*2-1=7种方案。
数据范围
100%的数据中N和M均不超过100
50%的数据中N和M至少有一个数不超过8
30%的数据中N和M均不超过6
分析:
这道题刚开始看时,我并没有多好的思路(托腮),只是去胡乱的打一打
这道题其实是是一道状压dp(说实话,我真没想到→_→)
这怎么是一道状压呢,下面来给大家解释一下。
首先,我们都知道每一列最多只有两个炮(为啥?仨炮都能轰着玩了好吧)
所以,这个时候也就有三个状态一个是这一列有0/1/2,所以我们就要想一想我们怎么设置状态
我们用 dp[i][j][k] 来表示状态,其中i表示行数,j表示有一个棋子的列数,k表示有两个棋子的列数。
这时候我们就来考虑状态转移方程
首先,不放棋子的话就是 dp[i][j][k]+=dp[i-][j][k];
其次放一个棋子的情况
if(k>=)dp[i][j][k]+=(dp[i-][j+][k-]*(j+));//选择放一个棋子,且放在有一个棋子的列上
if(j>=)dp[i][j][k]+=(dp[i-][j-][k]*(m-j-k+));//选择放一个棋子,且放在没有棋子的列上
放两个棋子:
if(k>=)dp[i][j][k]+=(dp[i-][j][k-]*j*(m-j-k+));//选择放两个棋子,一个放在有棋子的列上,一个放在没有棋子的列上。
if(j>=)dp[i][j][k]+=(dp[i-][j-][k]*C(m-j-k+));//选择放两个棋子,全部放在没有棋子的列上。
if(k>=)dp[i][j][k]+=(dp[i-][j+][k-]*C(j+));//选择放两个棋子,全部放在有一个棋子的列上。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
template <typename type>
void scan(type &x){
type f=;x=;char s=getchar();
while(s<''||s>''){if(s=='-')f=-;s=getchar();}
while(s>=''&&s<=''){x=x*+s-'';s=getchar();}
x*=f;
}
ll dp[][][];
ll n,m,mod=,ans;
ll C(ll x){
return (x*(x-)/)%mod;
}
int main(){
scan(n),scan(m);
dp[][][]=;
for(int i=;i<=n;i++){//表示每一行
for(int j=;j<=m;j++){
for(int k=;k<=m-j;k++){
dp[i][j][k]+=dp[i-][j][k];
if(k>=)dp[i][j][k]+=(dp[i-][j+][k-]*(j+));//选择放一个棋子,且放在有一个棋子的列上
if(j>=)dp[i][j][k]+=(dp[i-][j-][k]*(m-j-k+));//选择放一个棋子,且放在没有棋子的列上
if(k>=)dp[i][j][k]+=(dp[i-][j][k-]*j*(m-j-k+));//选择放两个棋子,一个放在有棋子的列上,一个放在没有棋子的列上。
if(j>=)dp[i][j][k]+=(dp[i-][j-][k]*C(m-j-k+));//选择放两个棋子,全部放在没有棋子的列上。
if(k>=)dp[i][j][k]+=(dp[i-][j+][k-]*C(j+));//选择放两个棋子,全部放在有一个棋子的列上。
dp[i][j][k]%=mod;
}
}
}
for(int i=;i<=m;i++){
for(int j=;j<=m;j++){
ans+=dp[n][i][j];
ans%=mod;
}
}
printf("%lld\n",ans); return ;
}
持续更新(当然,如果我还记得的话,欢迎评论~_~)……