前言
如果大家之前学过“线性DP”的话,应该已经知道,动态规划的过程是随着“阶段”的增长,在每个状态维度上不断扩展的。在任意时刻,已经求出最优解的状态与尚未求出最优解的状态在各维度上的分界点组成了 DP 扩展的“轮廓“。对于某些问题,我们需要在动态规划的”状态“中记录一个集合,保存这个”轮廓“的详细信息,以便进行状态转移。若集合大小不超过 N,集合中每个元素都是小于 K 的自然数,则我们可以把这个集合看作一个 N 位 K 进制数,以一个 [0,K^N - 1] 之间的十进制整数的形式作为 DP 状态的一维。这种把集合转化为整数记录在 DP 状态中的一类算法,被称为状态压缩动态规划。
具体例子
比如 “最短 Hamilton 路径” 问题,整个状态空间可看作 N 维,每维代表一个节点,只有 0(尚未访问) 和 1(已访问) 两个值。我们可以想象 DP 的 “轮廓” 以 “访问过的节点数目” 为阶段,从(0,0,0...0) 扩展到 (1,1,1...1)。为了记录当前状态在每个维度上的坐标是 0 还是 1,我们使用了一个 N 位二进制数,即 [0,2^N - 1] 之间的十进制整数存储节点的访问情况。另外,为了知道最后经过的节点是哪一个,我们把该节点编号作为附加信息,也保存在 DP 的状态中。因此,该状态压缩 DP 的状态压缩数组就由大小分别为 2^N 和 N 的两个维度构成。
炮兵阵地
本题是在矩形网格中放置图形的问题,即求最多能放多少个“十字形状”,并且每个“十字”的中心都不被其他“十字”覆盖。因此,我们采用按“行号”为阶段的 DP 方法。
解题思路
因为每个位置能否放置炮兵与它上面两行对应位置上是否放置炮兵有关,所以在向第 i 行的状态转移时,需要知道第 i - 1 行和第 i - 2 行的状态。我们把每一行的状态看作一个 M 位二进制数,用一个 0~2^M - 1 之间的十进制整数存储,其中第 p(0≤p<M) 位为 1 表示该行第 p 列放置了炮兵,为 0 则表示没有放置炮兵。
我们在 DP 前预处理出集合 S,存储 “相邻两个 1 的距离不小于 3” 的所有 M 位二进制数,这些二进制数代表每一行中两个炮兵的距离不能小于 3。
设 count(x) 表示 M 位二进制数 x 中 1 的个数。
设 valid(i,x) 表示 M 位二进制数 x 属于集合 S,并且 x 中的每个 1 对应在地图第 i 行中的位置都是平原。
设 F[i,j,k] 表示第 i 行压缩后的状态为 j,第 i - 1 行压缩后的状态为 k 时,前 i 行最多能摆放多少个炮兵。
初值:F[0,0,0] = 0,其余为负无穷。
虽然 M 位二进制数有 2^M 个,但只有集合 S 中的数才可能时合法状态。通过写程序预处理可以发现,事实上 S 集合非常小,仅包含不到 100 个数。我们可以对集合 S 中的数离散化,只对这些状态进行存储和遍历。时间和空间复杂度均为 O(N|S|^3)。
代码示例
#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,k;
int ans;
int cnt;// 表示状态总数
int s[110];// s[i]的二进制表示每一行的H分布状态
int f[110][110][110];// f[i][j][k]表示第 i 行的状态为 k,第 i-1 行的状态为 j 时候,前 i 行最多能够安装的大炮数量
int sum[110];// sum[i] 表示第 i 种状态安装的大炮数量
int now[110];// now[i] 表示第 i 种状态
char c;
bool ok(int x){// 判断状态 x 是否符合,即是否会出现两个大炮间隔小于2
if(x&(x<<1)) return 0;
if(x&(x<<2)) return 0;
return 1;
}
int count(int x){// 求出状态 x 中安装了多少门大炮,x 的二进制有几个1。
int t=0;
while(x>0){
if(x&1) t++;
x>>=1;
}
return t;
}
void find(int n){// 预处理求出有可能的状态。
for(int i=0;i<(1<<n);i++){
if(ok(i)){
now[++cnt]=i;
sum[cnt]=count(i);
}
}
}
int main(){
cin>>n>>m;
memset(f,-1,sizeof(f));
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>c;
if(c=='H') s[i]|=(1<<j-1);
}
}
find(m);
for(int i=1;i<=cnt;i++)// 第一行的特殊情况,预处理
if(!(now[i]&s[1]))
f[1][1][i]=sum[i];
for(int i=2;i<=n;i++){
for(int j=1;j<=cnt;j++){// 枚举第 i 行的状态
if(now[j]&s[i]) continue;
for(int k=1;k<=cnt;k++){// 枚举第 i-1 行的状态。
if(now[j]&now[k]) continue;
for(int l=1;l<=cnt;l++){// 枚举第 i-2 行的状态。
if(now[j]&now[l]) continue;
if(f[i-1][l][k]==-1) continue;
f[i][k][j]=max(f[i][k][j],f[i-1][l][k]+sum[j]);
}
}
}
}
for(int i=1;i<=cnt;i++){
for(int j=1;j<=cnt;j++){
ans=max(ans,f[n][i][j]);
}
}
cout<<ans<<endl;
return 0;
}