-
不得不说,状压DP是我比数位DP还要烂的板块。尽管其代码很短,但每次写的仍然漏洞百出,要调很久,还是太不熟练了。
-
以前一直搞不清楚状压与数位的区别,现在大概知道了:数位更专注于“数”,而状压只是一般而言将数据转化为二进制(当然也有三进制之类的神秘做法)方便转移及处理。
P1896 [SCOI2005] 互不侵犯
互不侵犯一直是比较经典的状压DP模板题。
-
显然对于某一行,其状态只由上一行所限制,也就是放的“王”不能互吃
同时,数据范围极小(这也是状压的一个特点),足够我们去枚举每一行放的情况。 -
那如何去表示状态呢?这里就体现出“状态压缩”的含义了。我们设
1
表示放了王0
表示没放,这样我们就可以方便地通过这种方式用一个数字去表示状态了。 -
如
0001 0101
就是一个状态,只是在计算中被压缩成了21
这个数而已。
这时DP状态就比较显然了。设 \(f_{h,lst,num}\) 表示当前统计到第 \(h\) 行,上一行的状态是 \(lst\),已经放了 \(num\) 个棋子的方案数。 -
而转移方式就比较显然了。由于时间上允许直接枚举状态,因此对于每个枚举的状态,都去判断与上一行是否满足条件,相符就继续搜,不相符就跳了。
由于我们是用二进制去维护的,我们可以方便地使用位运算左右移位去判断是否满足条件。
当我们枚举到第 \(n+1\) 行时,就代表前面的 \(n\) 行都算完了,因此直接判断是否刚好使用了 \(k\) 个棋子就行了,如果是就答案加一。 -
这里使用的是记忆化搜索的写法。事实上记忆化搜索是非常方便的DP实现方式,实现递推或者递归都比较直观、显然,可以避免循环极多层for的情况。
事实上,状压或者数位的记忆化搜索写法一般而言都比循环好看很多(可以看下某些循环题解,及其吓人) -
时间复杂度就很显然了。一般而言记忆化搜索的最劣复杂度就是其空间复杂度(因为重复搜到时都记忆化完走了,真正计算次数的量级就是空间)
因此对于此题而言,其空间时间复杂度都为 \(O(k\times n\times 2^n)\) -
注意当放置的国王数已经超过 \(k\) 的时候就不用搜了,可以直接返回。同时要注意同一行间放国王的限制。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll N;
ll n,k,f[12][1024][100];
ll dfs(ll last,ll h,ll sum)
{
if(sum>k||(h==n+1&&sum!=k)) return f[h][last][sum]=0;
if(h==n+1&&sum==k) return f[h][last][sum]=1;
if(f[h][last][sum]!=-1) return f[h][last][sum];
ll res=0;
for(ll i=0;i<N;i++)
{
ll num=0;
for(ll j=0;j<=n;j++) if((i>>j)&1) num++;
if(((last&(i<<1))==0)&&((last&(i))==0)&&(((last&(i>>1)))==0)&&((i&(i>>1))==0)&&((i&(i<<1))==0))
res+=dfs(i,h+1,num+sum);
}
return f[h][last][sum]=res;
}
int main()
{
memset(f,-1,sizeof(f));
cin>>n>>k;
N=1<<n;
cout<<dfs(0,1,0)<<endl;
return 0;
}
P2704 [NOI2001] 炮兵阵地
炮兵阵地同样是典题(但硬控我快三个月),但这道题大部分都是循环写法,洛谷上前面的题解没有一篇是相对简洁易懂的记搜写法。
-
同样与上一道题类似,只是“王”变了走法同时有地形限制,有些格子没办法放。
显然这里对于每一行的状态就由上两行转移而来了,同时同一行的要求更长。 -
考虑类似上一道题的状态,设同样是
1
为放了炮,0
为没放,则设
\(f_{dep,l1,l2}\) 。其中的 \(dep,l1,l2\) 分别表示已经正在统计第几行、上一行的状态、上上一行的状态。
而其本身代表 其本身这一行以及这一行下面所有行所能塞下的最多的炮的个数 -
为什么要这么设呢?考虑一下状压的本质,我们可以统计到这一行上方已经放置了多少个炮以及前两行的状态,
因此我们需要一个状态来记录这个确定的状态下其本身这一行及下方所有行所可能放置的最多的炮数,这样就可以快速统计答案。
即设其上方已经放了 \(up\) 个炮了,其下方最多放 \(down\) 个,那 \(ans\) 就是 \(\max(ans,up+down)\) -
很显然,不是吗?现在只需要统计 \(down\) 就可以了(\(up\) 当然是上面传下来的)
求法就与上一题几乎完全一样了。边界同样是 \(dep=n+1\) 时,这一行什么都不能放,因此就统计一下答案然后退出了(返回0是因为这一行不能对上一行造成任何贡献)
对于地图本身的限制,只需要先把地图的每一行同样压成二进制,枚举状态的时候判断一下有没有重叠即可。 -
空间时间复杂度都是 \(O(n\times (2^m)^2)\)
然后就爆完了。 -
怎么优化一下呢?发现同行之间的限制很强,大部分的状态都没有用。
因此考虑只枚举那些有用的,那些同行间都互斥的显然没有任何必要去枚举了。
打一下表发现有用的甚至不到一百个,因此枚举的时候就枚举有用的序号,预先将有用的存储在数组中(注意状态中的 \(l1,l2\) 也就变成了序号而不是状态本身了) -
然后负责度就变成 \(O(n\times w^2)\) 了,其中 \(w\) 是有用的状态数,不到100,然后就能过了。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1023;
const int N=105;
const int M=15;
int mp[N],n,m,yu[inf],cnt=0,f[N][N][N],ans;
int dfs(int dep,int l1,int l2,int up)
{
if(dep==n+1) {ans=max(ans,up);return 0;}
if(f[dep][l1][l2]!=-1) return f[dep][l1][l2];
int down=0;
for(int i=1;i<=cnt;i++)
if((((yu[i]&yu[l1]))||((yu[i]&yu[l2]))||((yu[i]&mp[dep]))||(yu[l1]&yu[l2]))) continue;
else{
int tmp=0;for(int j=0;j<m;j++) tmp+=(yu[i]>>j)&1;
int res=dfs(dep+1,i,l1,up+tmp);
down=max(down,tmp+res);
}
ans=max(ans,up+down);
return f[dep][l1][l2]=down;
}
signed main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;char op;memset(f,-1,sizeof(f));
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>op,mp[i]<<=1,mp[i]|=(op=='H');
for(int i=0;i<=(1<<m)-1;i++) if(!((i&(i<<1))||(i&(i<<2))||(i&(i>>1))||(i&(i>>2)))) yu[++cnt]=i;
int tmp=dfs(1,0,0,0);
cout<<ans<<'\n';
return 0;
}