轮廓线 DP
模拟赛中遇到了,被迫花了一天来学习。
轮廓线 DP 本质上是逐格转移的状压 DP,有特别经典的图可以说明:
思想上非常好理解,就是把分界处的状态压缩起来。主要是实现上有许多技巧需要说一下。第一个是状态的表示方法,在大多数情况只需要用 0/1 来代表这个位置是否存在插头即可,一般会以二进制或者四进制的形式压缩成一个整型(\(1\) 到 \(n\) 位是对应和纵向插头,而第 \(0\) 位放横向插头),操作的时候视情况展开成一个数组或者就直接对位进行操作。编码形式也比较重要,为了让状态确定唯一又尽量少,就需要采用一些优秀的编码形式。目前知道的有三种:无脑 \(01\),适用于没有太多要求的场景;括号序列,适用于强制要求只有一个回路的场景;最小表示法,适用于希望选择一个连通块的场景。特别地,对于第三种,每次 insert 的时候都需要转化成最小表示法的形式(因为连通块之间是没有顺序的,所以可以多压一)。
然后是状态是转移方法,一般而言轮廓线 DP 每个阶段存在的合法状态不会特别多,所以可以考虑把合法状态哈希存储,然后在下一个阶段就只需要找出哈希表里所有的元素即可,发现这一部分是可以滚动的,进一步压缩空间(29009
是一把比较顺眼的钥匙)。对于一个状态,要转移到下一个阶段,就只需要提取出这个状态对应的上插头和左插头,然后加上自己的状态进行一大堆分类讨论就可以了。然后结合例题分析,毕竟剩下的东西因题而异。放个我的哈希板子:
struct node{int pl,data,nxt;};
struct Hash{
node data[M];int nowCnt,head[H];
inline void clear(){nowCnt=0;memset(head,0,sizeof(head));}
inline void insert(int pl,int val){
int key=pl%H;
for(int i=head[key];i;i=data[i].nxt)if(data[i].pl==pl)return add(data[i].data,val);
data[++nowCnt]=(node){pl,val,head[key]};head[key]=nowCnt;
}
}h[2];
【模板】插头dp 插头 DP 的板子,由于强制要求只有一条闭合折线,所以需要用到括号表示法。特别的是由于希望覆盖满所有的格子,统计答案是时候只能在最后一个格子统计,并且此时强制上插头和左插头配对(这样才能做到不重不漏)。另外,如果当前格子恰好是两个左括号或者两个右括号时需要调转其中一个配对处的方向(调转上方显然好写,需要写个小小的函数来找到配对位置在哪)。然后就没了。
Eat the Trees 和上一道题很像,只是少了强制一条的限制,这样一来面对两个插头就不需要分类讨论左括号还是右括号了,所以状态的每一位只需要记录一个值即可。然后就不需要哈希了。
神秘的生物 最小表示法的应用,由于希望最后只有一个连通块,所以在上方有插头并且自己不选的时候要特别注意,如果自己不选会导致上插头断掉的话,那么这个状态就会是不合法的(判断上直接找轮廓线上有几个同类插头即可)。一个状态能贡献答案当且仅当轮廓线上只有一种插头(也就是当前只有一个连通块),然后就是要用最小表示法来缩减空间。
神奇游乐园 和板子是很像的,少了一些限制(每个格子不需要都选择,话说有了这个限制谁还搞 DP 啊),所以呢就多了一些决策,贡献答案的地方也就多了一些(要满足当前轮廓线恰好只有一对插头),然后就没什么了。
白金元首与莫斯科 也可以用状压来做。由于骨牌的结构是简单的,所以状态只需要记录一个布尔值即可,这样一来状态就非常稠密了,导致哈希大概率会被卡掉。转移上非常板。统计答案肯定不能每次暴力统计,考虑把这个点扣掉,那么当前点的答案就是从上往下到这个点的部分和从下往上到这个点的部分刚好可以拼起来并且不会延申到这个格子的方案,画图发现这等价于两个状态完全相同并且第 \(0\) 位为假,枚举状态累加答案即可。