介绍:
- 状压,顾名思义,就是采用位运算,来记录更多的必须记录的状态
- 状压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)
例一:
要点:
- 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[i−1][t][k−num[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 i−1行为状态为 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[i−1][p][q]+num[j])( q , p , j q,p,j q,p,j各自合理且之间均不发生冲突, 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);