插头dp
(真的太佩服了……陈丹琦在高中期间不仅发明了cdq分治,还发明了插头dp。
前置
插头dp实际上是状压的一种,一般用于处理路径连通性问题
轮廓线:已决策状态和未决策状态的分界线。
插头:一个格子某个方向的插头存在,表示这个格子在这个方向与相邻格子相连。
多条回路
例题:P5074 Eat the Trees
轮廓线长度是
转移的时候分类讨论,从当前格子左/上插头的存在情况推出右/下插头的存在情况。
考虑行间转移。结束状态会在最右侧边界,注意要保证它没有插头。删掉这个分界后加入下一行最开头的分界线即可。最后答案为
有前人经验:插头dp=巨型分讨
一条回路
以下是真正的模板题:P5056 [模板] 插头 DP
此时我们必须要求“一条回路”,跟整体的连通性有关,那么仅仅记录轮廓线上是否存在插头已经不够用了。所以我们需要新方法来记录轮廓线状态转移。
1.记录状态
1.1 最小表示法:
所有的障碍格子记为0,第一个非障碍格子和他连通的所有格子记为1,再找第一个未标记的非障碍格子和与它联通的格子记为2,重复操作直至所有格子标记完毕。
形式化的,我们对每一个连通块编号,每一个连通块的标号就是当前已有连通块的个数+1,所有连通块内的点的编号都和该连通块编号相同
1.2 括号表示法:
【性质】轮廓线上从左到右四个插头
即,“两两匹配”,“不会交叉”,容易想到括号匹配。
将轮廓线上每一条路径穿过它处,左边的插头标记为左括号,右边的插头标记为右括号,则左右括号一定一一对应。
于是我们就可以用三进制,0表示无插头,1表示左括号插头,2表示右括号插头,记录下所有的轮廓线信息。
注意到,括号表示法只对路径/回路等问题适用;连通块问题只能用最小表示法。
对这两种表示方法,我们一般都把它的状态表示为
且一般来说,我们的状态总数都会很大,但括号序列实际上并不会有太多合法状态,所以可以用哈希表来存储所有dp值。还得写滚动数组。
2.转移状态
令
其实和多条路径的分讨比较类似,但是需要注意的是我们需要讨论
建议多画几张图理解。
-
若
不能铺线,则只转移无插头的情况 -
,必须新建一对插头, -
或 ,则只能有下或右插头,且这个插头只能作为上一状态的延伸,此时把 的状态分别向 和 转移即可。 -
,我们只能合并这两个插头,因为不能再有延伸了。但还是需要对 具体的值分讨。-
,这两个插头合并会形成回路。但对于一条回路的题目,只有在 处形成这种转移。 -
,两个连通块被合并成了一个。 -
,我们将两个左端点合并了,为了保证括号序列的合法性,那么原来 对应的右端点就会变成新连通块的左端点。我们需要找到这个点并修改。 -
,同上。
-
(括号表示法)[模板] 插头dp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5,inf=2e9,p=9973;
int n,m,a[15][15],px,py;
struct hashtable{
int h[p],nxt[maxn],tot,s[maxn],dp[maxn];
void clear(){
tot=0;
for(int i=0;i<p;i++) h[i]=0;
}
void ins(int st,int val){
int x=st%p;
for(int i=h[x];i;i=nxt[i]){
if(s[i]==st){
dp[i]+=val;
return ;
}
}
nxt[++tot]=h[x],s[tot]=st,dp[tot]=val,h[x]=tot;
}
}f[2];
int getw(int s,int i){
return (s>>((i-1)<<1))&3;
}
int setw(int s,int i,int v){
return s^((getw(s,i)^v)<<((i-1)<<1));
}
int getl(int s,int i){
int res=0;
for(int p=i;p>0;p--){
int x=getw(s,p);
if(x==2) res++;
else if(x==1) res--;
if(!res) return p;
}
return 0;
}
int getr(int s,int i){
int res=0;
for(int p=i;p<=m+1;p++){
int x=getw(s,p);
if(x==1) res++;
else if(x==2) res--;
if(!res) return p;
}
return 0;
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
string s; cin>>s;
for(int j=1;j<=m;j++){
a[i][j]=(s[j-1]=='.')?1:0;
if(a[i][j]) px=i,py=j;
}
}
int lst=0,now=1,ans=0;
f[lst].ins(0,1);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=1;k<=f[lst].tot;k++){
int s=f[lst].s[k],v=f[lst].dp[k];
int x=getw(s,j),y=getw(s,j+1);
if(!a[i][j]){
if(!x&&!y) f[now].ins(s,v);
}
else{
if(!x&&!y) f[now].ins(setw(setw(s,j,1),j+1,2),v);
else if(!x||!y){
f[now].ins(s,v);
f[now].ins(setw(setw(s,j,y),j+1,x),v);
}
else{
int nxt=setw(setw(s,j,0),j+1,0);
if(x==1&&y==2){
if(i==px&&j==py&&!nxt) ans+=v;//最后一个格子,统计答案
}
else if(x==2&&y==1) f[now].ins(nxt,v);
else if(x==1&&y==1){
nxt=setw(nxt,getr(s,j+1),1);
f[now].ins(nxt,v);
}
else{
nxt=setw(nxt,getl(s,j),2);
f[now].ins(nxt,v);
}
}
}
}
swap(lst,now);
f[now].clear();
}
for(int k=1;k<=f[lst].tot;k++){
int s=f[lst].s[k],v=f[lst].dp[k];
if(!getw(s,m+1)) f[now].ins(s<<2,v);
}
swap(lst,now);
f[now].clear();
}
printf("%lld",ans);
return 0;
}
例题
跟板子差不多的题
-
[bzoj3125]CITY
-
[HNOI2004]邮递员
-
[HNOI2007]神奇游乐园
[SCOI2011]地板
其实这题跟回路的题也是类似的,只不过它算是“路径”
dp的题目重点都在于状态设计,把插头未转弯设为1,转过弯设为2,无插头设为0
转移分讨类似
「CQOI2015」标识设计
和上一题很像,只不过这题是固定方向的。
审题:放且仅放三个“L”……
我们不妨把当前已有的L的数量加入我们dp的转移式子,就可以统计了
额外需要注意的是,位运算会炸int,需要写成 1ll<<x 这样!!!
[BZOJ2310] ParkII
[HNOI2007]神奇游乐园 的升级版
区别:一条路径,每个点最多经过一次
考虑到对于路径,特殊的点在于它有起点和终点,即有两个格子是只经过一次的。同样我们可以单独用一个四进制数来表示这样格子的数量,加入dp的维度中,继续讨论。
写着写着你就会发现:完蛋了,和起点和终点相连的格子不能确定是左还是右括号!!只能新加一个状态3记录和起点终点相连的了。。
太难绷,调了两个晚上,发现是少考虑一种情况:对于中途给加起点/终点的,要把它所配的另一个括号改成独立插头!!!
[Wc2008]游览计划
这题就是一个要求连通块的题了,不能简单地用括号序列维护,这就是要用最小表示法了。
另外,它也是斯坦纳树的经典例题。斯坦纳树是这样一类问题:带边权无向图上有几个(一般是越10个)关键点,要求选择一些边使得这些点在同一个连通块内,同时要求所选的边的边权和最小。
最小生成树是在给定的点集和边中寻求最短网络使所有点连通。而最小斯坦纳树允许在给定点外增加额外的点,使生成的最短网络开销最小。 —— oi-wiki
斯坦纳树也是一种状压dp,但在这道题上比插头dp最小表示法简单了很多。(摆了,偷懒了,写斯坦纳树了。(逃
斯坦纳树的dp转移有两个:
-
合并子树(枚举子集,在根节点合并)
-
找根(注意不是“换根”)
Q: 为什么是合并子树而不能直接单点去加?
A:注意到我们转移时并没有全部记录路径,所以在我们转移时不能确定这个单点离整个连通块最近的距离。
那么为什么不能随意置换根的位置,从而找到离某个单点最近的距离?
在“找根”这一步,我们只是考虑了它的根还可以延伸到哪里;而当前最优方案,并不能把这个连通块内的所有点为根时的贡献都更新到。
如果还是不理解,手模一下可以发现:如果有一个节点,想从单点转移,度数大于2时,直接就寄了。。。
本文作者:YYYmoon
本文链接:https://www.cnblogs.com/YYYmoon/p/18705337
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步