什么是插头DP
插头DP通常用来解决一些奇奇怪怪的棋盘问题,比如给你一些限制条件,让你求棋盘上的一个环,或者一个特殊形状。本质上是一种状压DP,其实是状压DP的升级版。这里就拿学长推荐的毒瘤题神奇游乐园为例吧。
一些概念
插头
给你一个网格图,还有一个没完成的环路,你会发现有左端点和右端点,端点可以在网格的四个边上的任意一边,我们称左端点为左插头或者开插头,类似地,称右端点为右插头或者闭插头。
轮廓线
我们的DP是一格一格地进行的,完成了DP的网格和未进行DP的网格形成了一条分界线,这条分界线就是轮廓线。
如图所示:蓝色的是轮廓线,黄色的是正在进行DP的格子,绿色的是插头。
如何进行DP
预处理
我们设 $f_{i,j,k}$ 表示当前处理格子为 $(i,j)$,状态为 $k$ 时最大的满意度之和(题目要求的)。
Q:等等,什么叫“状态为 $k$”?
A:这里,$k$ 是一个四进制数,每一位都代表着轮廓线上每一小段的状况,这一位为 $0$ 代表这一段没有插头(就像上图的第三行第二列),这一位为 $1$ 代表这一段有一个开插头(就像上图的第三行第一列),这一位为 $2$ 代表这一段有一个闭插头(就像上图的第二行第四列)。那么,上图的状态就是 101220
。
Q:既然没有 $3$,那为什么不用三进制?
A:辣你自己写三进制位运算啊!用四进制的好处就是,它可以和二进制联系起来,这样就不用预处理啦!具体来说就是,假如要取四进制下的第 $k$ 位,那就是二进制的第 $2k$ 位和第 $2k+1$ 位。
那现在就把代码写出来吧:
1 | inline int get(int x,int k){//取出二进制状态 x 的第 k 位 |
Q:等等,这是什么鬼我看不懂啊!
A:那我解释一下:k<<1
意思就是 k*2
,那么 x>>(k<<1)
意思就是状态 $x$ 的第 $2k$ 位,又由于前面说的,要取出 $2k$ 和 $2k+1$ 位,就和 3(在二进制下是 11)进行按位与就行了。
状压DP的一个重要步骤就是预处理,所以我们先预处理出所有可行的状态。
Q:什么叫可行?
A:一个状态可行,那么它必须满足以下的条件:
- 不可能出现 3。(废话)
- 从左往右扫,遇到一个闭插头,就必须与他左边最近的未配对的的开插头配对。(仔细想想为什么)
- 所有插头都配对了。(废话)
我们用 s[]
数组记录插头的位置,Map[i][j]
表示状态 $i$ 下 $j$ 配对的插头,sta[]
记录合法的状态。
那么写出代码:
1 | inline void prework(){ |
注解1:
maxk=1<<((m+1)<<1)
用数学语言表达就是 $maxk=2^{2(m+1)}=4^{m+1}$ ,表示有多少种状态;是m+1
而不是m
是因为轮廓线的长度是m+1
而不是m
(因为有一个小拐弯)。
递推过程
接下来是重头戏,也就是递推过程。状态转移需要分很多情况。
我们令 cur
为当前格上插头的状态,last
为当前格左插头的状态。
情况一:cur==0&&last==0
将当前格的下插头设为新的开插头,当前格右插头设为新的闭插头。
情况二:cur==0||last==0
两种方法:
- 把插头拉到格子右边,右插头的值就是原插头的值。
- 把插头拉到格子下边,下面的插头的值就是原插头的值。
情况三:cur==2&&last==1
一条环路结束了,更新答案。
情况四:cur==1&&last==2
删去插头代表的回路。
情况五:cur==last
两个插头同开同闭。那么首先清空匹配,然后重新分配。
具体来说,如果是两个闭插头在一起,把 last
对应的开插头改成闭插头;如果是两个开插头在一起,把 cur
对应的闭插头改成开插头。
不清楚的话可以在纸上模拟一下。
递推过程的代码
那么下面是递推过程的代码:
1 | inline void Update(int &x,int k){ |
完整代码
1 |
|