例1 炮兵阵地
所谓状态压缩, 就是用二进制的性质来表示状态.
比如说, 在本题中, 若某一行为HHPPHPPH
, 则可以用二进制数11001001
表示
若某一行为炮空炮炮空炮空空
, 则可以用二进制数10110100
表示
- 定义状态
dp[line][i][j]
:line
表示当前行的序号(实际上只需要0, 1, 2
),i
表示当前行的炮的排列,j
表示上一行的炮的排列. - 定义数组
sum[]
, 用以存储每一种炮的排列中包含的炮的总数 - 状态转移:
i
为当前行的炮排列,j
为上一行的炮排列,k
为上上行的炮排列.
dp[line%3][i][j]=max(dp[line%3][i][j],dp[(line-1)%3][j][k]+sum[i])
- 我们在设计算法时, 对于
i, j, k
可以采用枚举法, 即遍历00...0 ~ 11...11
的每一个取值, 此时显然i, j, k
的组合并不一定符合题意. 我可以从如下角度判断:
1. 炮排列是否满足地形条件: 若i&map[line] == 1
则不符合,j, k
情况类似
2. 某一行中是否存在炮的左右距离过近的情况: 若i&(i<<1) == 1
或i&(i<<2) == 1
则不符合条件,j, k
情况类似
3. 当前行的炮排列是否与前两行冲突: 若i&j == 1
或i&k == 1
或j&k == 1
则不符合条件.
代码实现
#include<iostream>
using namespace std;
int map[105];
int dp[3][1<<10][1<<10];
int sum[1<<10];
int n,m,ans=-1;
char x;
int getSum(int x){
int cnt=0;
while(x!=0){
if(x&1) cnt++;
x>>=1;
}
return cnt;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
cin>>x;
map[i]<<=1;
if(x=='H') map[i]+=1;
}
}
for(int i=0;i<(1<<m);i++) sum[i]=getSum(i);
for(int i=0;i<(1<<m);i++){
if(i&map[0]||i&(i<<1)||i&(i<<2)) continue;
dp[0][i][0]=sum[i];
}
for(int i=0;i<(1<<m);i++){
if(i&map[1]||i&(i<<1)||i&(i<<2)) continue;
for(int j=0;j<(1<<m);j++){
if(j&map[0]||j&(j<<1)||j&(j<<2)||j&i) continue;
dp[1][i][j]=sum[i]+sum[j];
}
}
for(int line=2;line<n;line++){
for(int i=0;i<(1<<m);i++){
if(i&map[line]||i&(i<<1)||i&(i<<2)) continue;
for(int j=0;j<(1<<m);j++){
if(j&map[line-1]||j&(j<<1)||j&(j<<2)||j&i) continue;
for(int k=0;k<(1<<m);k++){
if(k&map[line-2]||k&(k<<1)||k&(k<<2)||j&k||i&k) continue;
dp[line%3][i][j]=max(dp[line%3][i][j],dp[(line-1)%3][j][k]+sum[i]);
}
}
}
}
for(int i=0;i<(1<<m);i++){
for(int j=0;j<(1<<m);j++){
ans=max(ans,dp[(n-1)%3][i][j]);
}
}
cout<<ans;
return 0;
}
改进: 设置一个数组a[]
, 保存所有符合i&(i<<1) == 1
或i&(i<<2) == 1
的i
, 避免大量的重复计算.
#include<iostream>
#include<algorithm>
using namespace std;
int n,m;
int graph[101];
int dp[3][65][65];
int sum[1<<10];
int a[65];
int cnt=0;
char c;
int _max=0;
int get_sum(int x){
int cnt=0;
while(x!=0){
if(x&1) cnt++;
x>>=1;
}
return cnt;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){ //压缩地图
for(int j=1;j<=m;j++){
cin>>c;
if(c=='H') graph[i]|=1;
if(j!=m) graph[i]<<=1;
}
}
for(int i=0;i<(1<<m);i++){
if(i&i<<1||i&i<<2) continue;
a[cnt++]=i;
sum[i]=get_sum(i);
}
for(int i=0;i<cnt;i++){
if(a[i]&graph[0]) continue;
dp[0][i][0]=sum[a[i]];
}
for(int i=0;i<cnt;i++){
if(a[i]&graph[1]) continue;
for(int j=0;j<cnt;j++){
if(a[j]&graph[0]||a[j]&a[i]) continue;
dp[1][i][j]=sum[a[i]]+sum[a[j]];
}
}
for(int line=2;line<n;line++){
for(int i=0;i<cnt;i++){
if(a[i]&graph[line]) continue;
for(int j=0;j<cnt;j++){
if(a[j]&graph[line-1]||a[i]&a[j]) continue;
for(int k=0;k<cnt;k++){
if(a[k]&graph[line-2]||a[i]&a[k]||a[j]&a[k]) continue;
dp[line%3][i][j]=max(dp[line%3][i][j],dp[(line-1)%3][j][k]+sum[a[i]]);
}
}
}
}
for(int i=0;i<cnt;i++){
for(int j=0;j<cnt;j++){
_max=max(_max,dp[(n-1)%3][i][j]);
}
}
cout<<_max;
return 0;
}
例2 互不侵犯
- 定义状态
dp[k][i][s]
: 在仅考虑前k
行的情况下, 第k
行的国王排列方式为i
, 前k
行的国王总数为s
的情况数 - 状态转移: 设上一行的状态为
j
,dp[k][i][s] = sum(dp[k-1][j][s-sum[i])
- 类似例1, 我们同样需要枚举
i, j
的所有情况, 然后排除掉不和题目要求的情况(判断条件见代码)
代码实现
#include<iostream>
using namespace std;
long long dp[10][1<<9][100];
int n,m;
int sum[1<<9];
long long ans=0;
int getSum(int x){
int cnt=0;
while(x!=0){
if(x&1) cnt++;
x>>=1;
}
return cnt;
}
int main(){
cin>>n>>m;
for(int i=0;i<1<<n;i++){
sum[i]=getSum(i);
}
for(int i=0;i<1<<n;i++){
if(i&(i<<1)) continue;
dp[0][i][sum[i]]++;
}
for(int k=1;k<n;k++){
for(int i=0;i<1<<n;i++){
if(i&(i<<1)) continue;
for(int j=0;j<1<<n;j++){
if(j&(j<<1)||j&i||j&(i<<1)||j&(i>>1)) continue;
for(int s=0;s<=m;s++){
dp[k][i][s]+=dp[k-1][j][s-sum[i]];
}
}
}
}
for(int i=0;i<1<<n;i++) ans+=dp[n-1][i][m];
cout<<ans;
return 0;
}
例3 愤怒的小鸟
题目分析
- 设抛物线的表达式为
y=ax^2+bx
, 待定系数a
和b
可通过两组x, y
来确定. - 我们任选两只小鸟
i
和j
, 通过两只小鸟的坐标求出对应抛物线的a, b
, 注意:a
应该小于0
, 然后我们遍历所有小鸟, 找出所有在这条抛物线上的小鸟. 由此, 我们得到了一个压缩的二进制数, 记录在line[i][j]
中. 我们可以想象到, 在某些特殊的题目设定下, 有些小鸟可能不能与任何其他小鸟构成抛物线. - 我们将小鸟存活状态定义为
i
, 枚举i ∈ [00...00, 11...11]
, 定义start[]
数组,start[i]
表示第一个目前未被击中的小鸟的序号. - 状态转移: 对状态
dp[i]
, 令j = start[i]
. 为了避免重复计算, 我们可以仅更新经过j
的状态. 于是dp[i|(1<<(j-1))]=min(dp[i|(1<<(j-1))],dp[i]+1)
,dp[i|line[j][k]]=min(dp[i|line[j][k]],dp[i]+1)
.
代码实现
#include<iostream>
#include<string.h>
#include<cmath>
using namespace std;
const double err=1e-8;
class BIRD{
public:
double x;
double y;
};
BIRD bird[20];
int line[20][20];
int dp[1<<18];
int start[1<<18];
int T, n, m;
void cal(double& a, double& b, int i, int j){
double x1=bird[i].x, x2=bird[j].x;
double y1=bird[i].y, y2=bird[j].y;
a=(y1*x2-y2*x1)/(x1*x1*x2-x2*x2*x1);
b=(y1*x2*x2-y2*x1*x1)/(x1*x2*x2-x2*x1*x1);
}
int getStart(int x){
for(int j=1;j<=18;j++){
if(!(x&(1<<(j-1)))) return j;
}
}
int main(){
cin>>T;
for(int i=0;i<1<<18;i++) start[i]=getStart(i);
for(int a=1;a<=T;a++){
memset(bird,0,sizeof(bird));
memset(dp,127,sizeof(dp));
memset(line,0,sizeof(line));
dp[0]=0;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>bird[i].x>>bird[i].y;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(fabs(bird[i].x-bird[j].x)<=err) continue;
double a, b;
cal(a,b,i,j);
if(a>err) continue;
for(int k=1;k<=n;k++){
if(fabs(bird[k].y-a*bird[k].x*bird[k].x-b*bird[k].x)<err){
line[i][j]|=1<<(k-1);
}
}
}
}
for(int i=0;i<1<<n;i++){
int j=start[i];
dp[i|(1<<(j-1))]=min(dp[i|(1<<(j-1))],dp[i]+1);
for(int k=1;k<=n;k++){
dp[i|line[j][k]]=min(dp[i|line[j][k]],dp[i]+1);
}
}
cout<<dp[(1<<n)-1]<<'\n';
}
return 0;
}