动态点分治学习笔记
前言
点分树是一种树上分治算法,其本质是在基于点分治的基础上构造出一棵重构树。
通常用于解决与原树结构无关的树上统计问题。
前置芝士
- 首先静态点分治是必然的。
- 因为维护数据是往往需要某些
毒瘤数据结构,所以可能要用到:动态开点线段树、树状数组、堆等。 - 同时 STL 也是代替手写数据结构的好工具。
百折不挠的 Debug精神。
算法原理
首先拿出模板题。
显然如果没有修改点权这一操作的话,离线下来就是裸的静态点分治。
多了修改之后,我们难道就只能每修改一次重新点分一次吗(好像挺方便的)
我们可以按照分治递归的顺序提一颗新树出来,易知树高是 \(O(\log n)\) 的。
具体地说,对于每一个找到的重心,将上一层分治时的重心设为它的父亲,得到一颗大小不变、最多 \(\log n\) 层的点分树。
既然它只有 \(\log n\) 层,那么我们即使暴力跳某个点的祖先也就 \(\log n\) 下,这样就很好的使修改操作变得可以乱搞可控起来。
对于以 \(i\) 为根的子树统计答案时,我们通常选择统计:
- \(f_1(i)\):以 \(i\) 为根的子树的答案。
- \(f_2(i)\):以 \(i\) 为根的子树对 \(fa_i\) 的答案贡献。
容斥之后发现:\(ans(fa_i)=f_1(fa_i)-f_2(i)+f_1(i)\)。
然后不断利用暴力统计+简单容斥,题目变得暴力简单起来。
代码实现
日常建树:
int sz[N],mxp[N],fa[N],rt;
bool vis[N];
void get_sz(int u,int fa){
sz[u]=1;for(int i=head[u],v;i;i=ed[i].nxt)
if(!vis[v=ed[i].to] && v!=fa) get_sz(v,u),sz[u]+=sz[v];
}
void get_root(int u,int fa,int Top){
mxp[u]=sz[Top]-sz[u];
for(int i=head[u],v;i;i=ed[i].nxt) if(!vis[v=ed[i].to] && v!=fa)
get_root(v,u,Top),mxp[u]=max(mxp[u],sz[v]);
if(!rt || mxp[u]<mxp[rt]) rt=u;
}
void dfs(int u,int last){
get_sz(u,0);rt=0;get_root(u,0,u);
u=rt;fa[u]=last;vis[u]=true;
get_sz(u,0);
for(int i=head[u],v;i;i=ed[i].nxt) if(!vis[v=ed[i].to]) dfs(v,u);
}
显然一般的建树不可能这么简单,这只是个简单框架。
建完之后就是跳树了。
void query(int u){
ans=以 u 为根的子树的答案。
for(int i=u;fa[i];i=fa[i]){
ans+=以 fa[i] 为根的子树的答案。
ans-=以 i 为根的子树对 fa[i] 的答案贡献。
}
}
同样也是简单框架。
值得注意的是对于与统计路径有关的点分树题,我们往往要不断求树上任意两点的距离。
方法有很多,这里推荐简单好写而且跑的很快的树剖:
struct DIS{
int dep[N],Fa[N],son[N],top[N],Sz[N];
void dfs1(int u,int fa){
dep[u]=dep[fa]+1;Fa[u]=fa;Sz[u]=1;
for(int i=head[u],v;i;i=ed[i].nxt) if((v=ed[i].to)!=fa){
dfs1(v,u);Sz[u]+=Sz[v];
if(Sz[v]>Sz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int Top){
top[u]=Top;if(!son[u]) return;dfs2(son[u],Top);
for(int i=head[u];i;i=ed[i].nxt){
int v=ed[i].to;if(v==Fa[u] || v==son[u]) continue;
dfs2(v,v);
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=Fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);return x;
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
void build(){dfs1(1,0);dfs2(1,1);}
}T;
//调用方法:
T.build();
dis(x,y)=T.dis(x,y);
以下还有几个容易错的实现细节:
- 子连通块大小最好不要用 \(sz(v)\),我上面采用的点分树写法巧妙避开了这个问题。
- 一般不用重新建树,如果需要建议将原树用结构体封装,避免混淆。
- 使用 STL 时注意边界条件,防止 RE。
时间复杂度
说了这么久,点分树的时间复杂度到底有多优秀呢。
显然它的复杂度与你数据结构的复杂度有关,我们假设数据结构的修改和查询操作的复杂度都是 \(O(\log n)\) 的。
同时询问次数不高于节点数量。那么点分树的时间复杂度为 \(O(n\log^2 n)\)。
例题
例题一
套路题,统计时利用树状数组 \(O(\log n)\) 修改并统计前缀和。
由于空间要求,我们需要动态开点。(树状数组怎么动态开点?STL 大法好)
利用 vector 即可实现,对于不熟悉 STL 的同学来说有些麻烦,但这也是熟悉 STL 的好机会。
例题二
题目大意:护一颗带点权、边权树,每次给出 \(x,l,r\),查询 \(\sum_{l\leq A_y\leq r} dis(x,y)\) ,其中 \(A_y\) 为 \(y\) 的点权。
套路题,注意统计路径长度时常常还要统计节点数量,因为 \(Edge(i,fa_i)\) 是需要另外容斥的。
针对这道题,如果没有强制在线的话,可以离线下来将点权离散化后用动态开点的线段树实现。
但是对于强制在线就只能vector+二分+前(后)缀和。
为什么是后缀和呢,因为 lower_bound 和 upper_bound 都是左闭右开,使用后缀和更加简便。
例题三
可以得出结论:
将 \(x\) 换为其子节点 \(y\) 更优时,当且仅当 \(2\times sum(y)>sum(1)\)。
这里假设 \(1\) 为根节点,\(sum(i)\) 表示以 \(i\) 为根的子树的点权之和。
显然符合条件的 \(y\) 节点仅有一个。
那么我们利用点分树暴力往下找即可,这是一类比较特殊的点分树题。
总结一下就是利用点分树暴力统计。
例题四
丧心病狂的推柿子题,推推柿子就好。
推完之后发现和上一题代码几乎一模一样,就是多维护几个东东。
例题五
最简单的放最后
套路题,唯一难点是我们需要一个支持插入、查询最大值、查询次大值、删除某个元素的数据结构。
一个比较有技巧性的操作是用两个堆实现惰性删除,需要比较大的代码实现能力。
注意常数较大。
总结
参考资料。
点分树还可以结合很多数据结构,所以数据结构的学习是关键之一。
很多点分树的题目同样可以用 LCT 解决,要继续学习。
或许我们还可以来一道大毒瘤
完结撒花。

浙公网安备 33010602011771号