(广义)圆方树 & 仙人掌 学习笔记
(广义)圆方树 & 仙人掌 学习笔记
前言
圆方树可用于分解仙人掌,同时也可以拿来处理点双,是一个不能错过的知识点。
实现
先对一个无向图跑 Tarjan 求出点双,然后定义:
- 圆点:就是原点。
- 方点:每个点双就是一个方点。
然后我们让方点与自己点双中的所有圆点都连上边,就得到了圆方树。
应用
实现很简单,但是应用并不一定。
圆方树省略
建出圆方树是一件非常耗时的事,有的时候这步可以直接省略,帮我们在考场上节约很多时间。
P10779 BZOJ4316 小 C 的独立集
在仙人掌上进行 DP,但其实可以不建出圆方树,直接在 Tarjan 算法执行的时候 DP。
我们定义在仙人掌上:
- 叶片:大小大于 \(2\) 的点双;
- 树边:大小等于 \(2\) 的点双中连接两原点的边;
我们假设现在正在跑 Tarjan,进行到了点 \(u\)。那么对于点 \(u\) 的某个子节点 \(v\),与 \(u\) 的关系有两种可能:
- 与 \(u\) 不同在任何一个叶片上,即 \((u,v)\) 为树边。
- 与 \(u\) 同在某一个叶片上,假设它们同在的叶片为 \(leaf\),则 \(leaf\) 中有两个点与 \(u\) 有连非树边。
那么回到题目中,我们对于以上两种 \(v\) 分别进行 DP 处理:
-
第一种很简单,就如同树上的独立集:
我们可以在遍历子节点的时候直接判断出来。
for(const int &v:g[u])if(v^fa[u]) { if(!dfn[v]) { fa[v]=u,tarjan(v),tomin(low[u],low[v]); if(low[v]>dfn[u])f[u][0]+=max(f[v][0],f[v][1]),f[u][1]+=f[v][0]; } else tomin(low[u],dfn[v]); } -
第二种需要我们遍历叶片:
先找到 \(leaf\) 中的某个与 \(u\) 相连的 \(v\),这个 \(v\) 又分成两种情况:
- 在搜索树中父节点为 \(u\)。
- 在搜索树中父节点不为 \(u\)。
我们可以从“在搜索树中父节点不为 \(u\)”的这一类 \(v\) 入手,发现通过仙人掌上的每个点双都是一个环的性质可以通过不断跳父节点来遍历整个点双。
最后我们只要枚举 \(u\) 是否选,然后进行环上的 DP 即可。
void DP(int u,int v) { int F[2] {0,0}; for(int w(v); w^u; w=fa[w]) { int G[2] {F[0]+f[w][0],F[1]+f[w][1]}; F[0]=max(G[0],G[1]),F[1]=G[0]; } f[u][0]+=F[0]; F[0]=0,F[1]=-INF; for(int w(v); w^u; w=fa[w]) { int G[2] {F[0]+f[w][0],F[1]+f[w][1]}; F[0]=max(G[0],G[1]),F[1]=G[0]; } f[u][1]+=F[1]; }for(const int &v:g[u])if(fa[v]!=u&&dfn[u]<dfn[v])DP(u,v);
void DP(int u,int v) {
int F[2] {0,0};
for(int w(v); w^u; w=fa[w]) {
int G[2] {F[0]+f[w][0],F[1]+f[w][1]};
F[0]=max(G[0],G[1]),F[1]=G[0];
}
f[u][0]+=F[0];
F[0]=0,F[1]=-INF;
for(int w(v); w^u; w=fa[w]) {
int G[2] {F[0]+f[w][0],F[1]+f[w][1]};
F[0]=max(G[0],G[1]),F[1]=G[0];
}
f[u][1]+=F[1];
}
void tarjan(int u) {
dfn[u]=low[u]=++idx,f[u][0]=0,f[u][1]=1;
for(const int &v:g[u])if(v^fa[u]) {
if(!dfn[v]) {
fa[v]=u,tarjan(v),tomin(low[u],low[v]);
if(low[v]>dfn[u])f[u][0]+=max(f[v][0],f[v][1]),f[u][1]+=f[v][0];
} else tomin(low[u],dfn[v]);
}
for(const int &v:g[u])if(fa[v]!=u&&dfn[u]<dfn[v])DP(u,v);
}
P4244 [SHOI2008] 仙人掌图 II
这是一道求仙人掌图直径的题目,我们尝试用一次 DP 的方法求解。
那么思路与上题相似,在树边直接 DP,在叶片上单调队列处理即可。
void Solve(int u,int v) {
int h(1),t(0),len(0);
static int q[N<<1],val[N<<1];
for(int w(v); w^fa[u]; w=fa[w])val[++len]=f[w];
FOR(i,1,len)val[len+i]=val[i];
FOR(i,1,len<<1) {
while(h<=t&&(i-q[h])>(len>>1))++h;
if(h<=t)tomax(ans,val[i]+i+(val[q[h]]-q[h]));
while(h<=t&&(val[q[t]]-q[t])<(val[i]-i))--t;
q[++t]=i;
}
FOR(i,1,len)tomax(f[u],val[i]+min(i,len-i));
}
void tarjan(int u) {
dfn[u]=low[u]=++idx;
for(const int &v:g[u])if(v^fa[u]) {
if(!dfn[v]) {
fa[v]=u,tarjan(v),tomin(low[u],low[v]);
if(low[v]>dfn[u])tomax(ans,f[u]+f[v]+1),tomax(f[u],f[v]+1);
} else tomin(low[u],dfn[v]);
}
for(const int &v:g[u])if(u!=fa[v]&&dfn[u]<dfn[v])Solve(u,v);
}
P3180 [HAOI2016] 地图
这道题如果用线段树合并,那么离线询问后在 Tarjan 时直接合并和回答会节约很多代码。
仙人掌上求最短距离
P5236 【模板】静态仙人掌
先建出圆方树,然后对于一个点双,会有一个圆点在圆方树上是方点的父节点,其余都是方点的子节点,如下图。
那么我们可以将方点到父节点的距离赋值为 \(0\),其余的圆点到方点的权值都赋为它们在叶片上到方点父节点的距离,这个可以预处理后 \(O(1)\) 在线查询。然后我们树上前缀和加起来,得到 \(\{dis_i\}\)。
预处理之后来看看查询:假设现在查询的是 \(u,v\) 的距离,求得它们在圆方树上的祖先为 \(pa\)。
- \(pa\) 为圆点:那么答案就是 \(dis_u+dis_v-2dis_{pa}\)。
- \(pa\) 为方点,那么 \(u,v\) 同在以 \(pa\) 为方点的点双上:设 \(su,sv\) 分别为 \(pa\) 的子节点中最接近 \(u,v\) 的,答案为 \((dis_u-dis_{su})+(dis_v-dis_{sv})+Dis_{pa}(su,sv)\),\(Dis_{pa}(su,sv)\) 表示在 \(pa\) 这个点双上, \(su,sv\) 的最近距离。
1045C - Codeforces
上题的弱化版。虽然不是仙人掌了,但是可以转化成仙人掌的模型。
圆方树上计数
在圆方树上计数很容易计算重复,有的时候我们可以运用圆点和方点交替出现的性质来求解。
P4630 [APIO2018] 铁人两项
明显地,先对点双建出圆方树,然后考虑计数。
如果我们考虑在方点计算 \(c\) 处于当前点双时的所有情况,似乎去重非常麻烦,那怎么办?我们可以在圆点也计数,并从答案中减去计出的数,就可以做到容斥去重,这题就变得十分简单。
void tarjan(int u,int fa) {
dfn[u]=low[u]=++idx,siz[st[++top]=u]=-1,++cur;
EDGE(g,i,u,v)if(v^fa) {
if(!dfn[v]) {
tarjan(v,u),tomin(low[u],low[v]);
if(low[v]>=dfn[u]) {
siz[++dc]=1,G.att(u,dc,0);
do ++siz[dc],G.att(dc,st[top],1);
while(st[top--]^v);
}
} else tomin(low[u],dfn[v]);
}
}
void dfs(int u) {
Siz[u]=(u<=n);
EDGE(G,i,u,v)dfs(v),ans+=(ll)2*Siz[u]*Siz[v]*siz[u],Siz[u]+=Siz[v];
ans+=(ll)2*Siz[u]*(cur-Siz[u])*siz[u];
}
虚仙人掌
其实就是在圆方树上建虚树。
P4606 [SDOI2018] 战略游戏
比较简单的计数题。
mx的仙人掌
【集训队互测2016】火车司机出秦川
点分治
【UR #1】跳蚤国王下江南 - 题目
点分治配合 FFT,分治中心还要找带权中心。
仙人掌链剖分
【清华集训2015】静态仙人掌 - 题目
注:这题可以用 bitset 做,更简单。
这题将仙人掌上所有点到根的最短和最长距离都固定,让你带修查询。剖分的重点是把仙人掌上的点(不是圆方树上的)分成三类,不过这个我们后面会讲。
-
还是像在树上树剖一样在圆方树上 DFS 计算各个子树的大小并找出重儿子。
-
第二次 DFS,但是与在树上树剖有很大区别:
- 对于根节点,它肯定是个圆点:它的 DFS 序为 \(1\),\(top_{rt} = rt\)。
- 对于方点:它不计入 DFS 序,因为没有必要,但是树剖的时候会把它放进去一起剖,这个不影响复杂度。同时它的子节点的 DFS 序都需要它来赋,我们按环上的顺序给它的子节点赋。
- 对于非根的圆点,没有什么特殊的。
这些点遍历子节点时都要先遍历重儿子(如果存在)。
我们给点分类是在方点给子节点赋值时分类:
- 根节点和各个方点的重儿子都分为 \(0\) 类点。
- 对于一个方点的所有轻儿子,将重儿子到根节点最短路径上的点分为 \(1\) 类点。
- 对于一个方点的所有轻儿子,将重儿子到根节点最长路径上的点分为 \(2\) 类点。
然后在修改时我们把它们分开就可以做到不重不漏。
-
修改,这一步需要的判断较多:
对于 \(u\),我们在修改它到其链顶 \(top_u\) 时要注意:
-
如果 \(top_u\) 是根,将它所有种类的点都修改了。
-
如果 \(top_u\) 是非根圆点,将它所在的那个点双上所经过所有种类的点都修改了。
-
最后要判断 \(u\) 是不是圆点:
- 是圆点:修改 \(u\) 到链顶的子节点。
- 是方点:修改 \(u\) 的父节点到链顶的子节点。
修改时注意修改的点的类型,\(0\) 类点是一定要修改的。
-
回到本题,修改的信息用线段树维护即可。
具体的还有一些东西需要维护:提交记录 #746768 - Universal Online Judge (uoj.ac)。
结合其他算法
487E - Codeforces
这题可以结合可删堆和线段树,比较简单。
P3180 [HAOI2016] 地图
这题可以结合线段树合并还有莫队。


浙公网安备 33010602011771号