这道题是一道状压 dp 的特别毒瘤的基础题(虽然我打了整整一个早上),但是因为每一个炮兵都会影响到之后的两行的放置,所以用状压去压两行,按行处理每一行的情况即可。每一行放置的时候也很简单,只需考虑这个位置前两行有没有放置炮兵以及这个位置是不是山丘即可。
那么首先,dp 方程可以很快推出来,dp[L][S][i]表示当前状态是 S,上一行的状态是 L,当前考虑到了第 i 行:
dp[L][S][i]=max(dp[L][S][i],dp[FL][L][i-1]+Sum[S]); 这里 FL 表示上上行的状态,Sum[S] 表示当前状态 S 里面包含几个 1。
那么有了这个 dp 方程后,就可以愉快的递推了,不过这道题有几个细节需要注意一下:
1.判断每个位置是不是山丘
这个很好解决,只要把每一行的输入都转成一个二进制数(平原是 0,山丘是1),然后直接跟待判断的状态做一次位运算即可,就是 S&a[i],如果位运算结果不是零,说明有些位置放在了山丘上,也就是说当前状态不合法。
2.判断每个状态有没有两个炮兵左右距离在两格之内
这个需要动脑想一下,我们发现一个神奇的结论,如果把表示当前状态的二进制数位运算左移一位,那么用这个结果与原状态做一次位运算与操作,如果结果不是 0,那么就一定存在两个炮兵左右距离在一格之内。同理,左移两位就可以判断左右距离在两格之内。这个过程也就是 S&(S<<1),S&(S<<2)。
3.判断每一列之前两行有没有炮兵
这个就直接用当前状态分别与之前的两行即可,就是 S&L,S&FL,如果与操作结果不为零,说明有若干列前两行有炮兵,也就是说当前状态不合法。
最后说一句,一定要用滚动数组(因为只用到每一行和前两行,所以只用滚动三行),否则会 MLE 0 (我当初就是这么惨)。。。
好了细节都说完了,下面就是具体的代码实现了:
#include<iostream>
using namespace std;
int n,m,ans,dp[(1<<10)][(1<<10)][3]/*滚动数组*/,a[105],Sum[(1<<10)];
char x;
int getsum(int S) //当前状态 S 里面包含几个 1
{
int tot=0;
while(S) {if(S&1) tot++; S>>=1;}
return tot;
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
cin>>x,a[i]<<=1,a[i]+=(x=='H'?1:0); //转成二进制数
for(int i=0;i<(1<<m);i++)
Sum[i]=getsum(i); //初始化 Sum 数组
for(int S=0;S<(1<<m);S++)
if(!(S&a[0] || (S&(S<<1)) || (S&(S<<2))))
dp[0][S][0]=Sum[S]; //初始化
for(int L=0;L<(1<<m);L++)
for(int S=0;S<(1<<m);S++)
if(!(L&S || L&a[0] || S&a[1] || (L&(L<<1)) || (L&(L<<2)) || (S&(S<<1)) || (S&(S<<2)))) //谜之一长串特判
dp[L][S][1]=Sum[S]+Sum[L];
for(int i=2;i<n;i++)
for(int L=0;L<(1<<m);L++)
{
if(L&a[i-1] || (L&(L<<1)) || (L&(L<<2))) continue; //特判
for(int S=0;S<(1<<m);S++)
{
if(S&a[i] || L&S || (S&(S<<1)) || (S&(S<<2))) continue; //还是特判
for(int FL=0;FL<(1<<m);FL++)
{
if(FL&L || FL&S || FL&a[i-2] || (FL&(FL<<1)) || (FL&(FL<<2))) continue; //仍然是特判
dp[L][S][i%3]=max(dp[L][S][i%3],dp[FL][L][(i-1)%3]+Sum[S]); //滚动数组的实现方法
}
}
}
for(int L=0;L<(1<<m);L++)
for(int S=0;S<(1<<m);S++)
ans=max(ans,dp[L][S][(n-1)%3]); //结束状态可以是最后一行的任何状态
cout<<ans;
return 0;
}