全局平衡二叉树学习笔记

· · 算法·理论

\large\color{red}\text{Global Biased Tree}

据说这玩意写动态 \text{DP} 吊打树剖,不仅好写而且快。

于是抽了一点时间来学习一下。

一篇讲得很好的博客

另一篇讲得很好的博客 (有图!)

算法理解

就我个人看来,“全局平衡二叉树”其实相当于是 \text{LCT} 的静态版(别较真,我不知道哪个先有)——它将 \text{LCT} 的实链剖分和 \text{splay} 换成了重链剖分和\bold{bst},因此失去了改变树形的能力,但拥有了更小的常数。

\text{LCT} 类似的,“全局平衡二叉树”具有如下性质:

1.由很多棵二叉树通过轻边连起来组成,每一棵二叉树维护了原树的一条重链,其中序遍历的顺序就是这条重链深度单调递增的顺序。每个节点出现且仅出现在一棵二叉树中。

2.边分为重边和轻边,重边是包含在二叉树中的边。轻边从一颗二叉树的根节点指向它所对应的重链顶端节点的父节点。轻边“认父不认子”。

而这里,普通 $\text{bst}$ 并不能带来优秀的复杂度,我们要在这些棵~~不普通的~~ $\text{bst}$ 上下一些心思,使之具有一些性质(最精妙的地方)。 具体地,对一条重链,以**轻子树 $size$ 和 $+1$** 作为点权,每次求出**加权重心**作为根,再向以该点分开的左右两半递归建左右子树。最后将整条重链的树根连向该重链顶点的父亲。 这样虽然单个重链的二叉树不一定平衡,但是所有二叉树和轻边组成的树满足树高为 $O(\log n)$。因为每向上跳一条边(无论轻边还是重边),子树 $size$ 至少增加一倍。 ## 应用 “全局平衡二叉树”不算热门算法,很多时候也都可以用树剖代替。 但是在单点修改或链修改一些简单信息时都表现得十分优异,可以说是吊打树剖。 ## 实例 ### [【模板】动态 DP](https://www.luogu.com.cn/problem/P4719) 树,点带权,支持单点修改点权,求树上最大独立集。 朴素转移: $$ \begin{aligned} f[u][0]&=\sum \max(f[v][1],f[v][0])\\ f[u][1]&=w_u+\sum f[v][0] \end{aligned} $$ 另外定义 $p$ 为重儿子, $g[u][0/1]$ 为除去重儿子贡献的答案。 则: $$ \begin{aligned} g[u][0]&=\sum_{v!=p} \max(f[v][1],f[v][0])\\ g[u][1]&=w_u+\sum_{v!=p} f[v][0]\\ f[u][0]&=g[u][0]+ \max(f[p][1],f[p][0])\\ f[u][1]&=g[u][1]+ f[p][0]\\ \end{aligned} $$ 定义“加法”为取 $\max$,“乘法”为数的加法,可以证明(但我不会)这样的矩阵乘法满足结合律。 于是有: $$ \begin{bmatrix}g_{u,0}&g_{u,0}\\g_{u,1}&-\infty\end{bmatrix} \times \begin{bmatrix}f_{p,0}\\f_{p,1}\end{bmatrix} =\begin{bmatrix}f_{u,0}\\f_{u,1}\end{bmatrix} $$ 发现改一个点的点权会影响其到根的整条链上答案,但在原树上暴力跳最劣一次 $O(n)$。 如果树剖,利用跳到根只会经过 $O(\log n)$ 条轻边这一性质,用线段树维护重链,实现只在跳轻边时算答案,复杂度 $O(\log^2 n)$。 但是,在全局平衡二叉树上,直接暴力跳到根经过的点数就是 $O(\log n)$ 的!不需要做其他处理了,只用跳时分类讨论一下轻重边就行。 代码: ```cpp #include<bits/stdc++.h> using std::max; inline int rd(int x=0,int y=1,char c=getchar()) { for(;!isdigit(c);c=getchar())if(c=='-')y=-1; for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+(c^48); return x*y; } const int inf=0x3f3f3f3f; struct mat { int a[2][2]; mat(){memset(a,0,sizeof a);} int* operator[](int x){return a[x];} const int* operator[](int x)const{return a[x];} const mat operator*(const mat &b)const { mat res; res[0][0]=max(a[0][0]+b[0][0],a[0][1]+b[1][0]); res[0][1]=max(a[0][0]+b[0][1],a[0][1]+b[1][1]); res[1][0]=max(a[1][0]+b[0][0],a[1][1]+b[1][0]); res[1][1]=max(a[1][0]+b[0][1],a[1][1]+b[1][1]); return res; } int mx(){return max(a[0][0],a[1][0]);} void I(){a[0][1]=a[1][0]=-inf,a[0][0]=a[1][1]=0;} }; const int N=1e5+7; int n,w[N]; std::vector<int> G[N]; int son[N],siz[N],dfn[N],id[N],tot; void dfs1(int u,int fr) { siz[u]=1; for(int v:G[u]) if(v!=fr) { dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]])son[u]=v; } } void dfs2(int u) { dfn[u]=++tot; id[tot]=u; if(!son[u])return; dfs2(son[u]); for(int v:G[u]) if(!dfn[v])dfs2(v); } namespace GBT { struct node { int ls,rs,fa; mat g,f;//f只在重链链顶是真正的f }tr[N]; #define ls(p) tr[p].ls #define rs(p) tr[p].rs #define fa(p) tr[p].fa #define g(p) tr[p].g #define f(p) tr[p].f inline void init() { g(0).I();f(0).I(); for(int i=1;i<=n;++i) g(i)[1][0]=w[i],g(i)[1][1]=-inf; } inline void up(int p) { f(p)=f(ls(p))*g(p)*f(rs(p)); } inline void dvt(int p,int w)//重链链顶给它父亲贡献(w=-1表示撤销贡献) { int x=fa(p); g(x)[0][0]+=w*f(p).mx(); g(x)[0][1]=g(x)[0][0]; g(x)[1][1]+=w*f(p)[0][0]; } int cbld(int l,int r)//对一条重链建二叉树 { int sz=0,tot=0,x,i; for(i=l;i<=r;++i) tot+=siz[id[i]]-siz[son[id[i]]]; for(i=l;(sz<<1)<tot;++i) sz+=siz[id[i]]-siz[son[id[i]]]; x=id[--i]; if(i>l) ls(x)=cbld(l,i-1),fa(ls(x))=x; if(i<r) rs(x)=cbld(i+1,r),fa(rs(x))=x; up(x); return x; } int bld(int u,int fr)//建树 { int x=u,y=fr,z; for(;x;x=son[y=x]) for(int v:G[x]) if(v!=son[x]&&v!=y) fa(z=bld(v,x))=x,dvt(z,1); return cbld(dfn[u],dfn[y]); } void upd(int u,int x)//单点改 { g(u)[1][0]+=x-w[u]; w[u]=x; for(;u;u=fa(u)) if(u!=ls(fa(u))&&u!=rs(fa(u))) dvt(u,-1),up(u),dvt(u,1); else up(u); } } int main() { int m,i,x,y,RT; n=rd(),m=rd(); for(i=1;i<=n;++i) w[i]=rd(); for(i=1;i<n;++i) { x=rd(),y=rd(); G[x].push_back(y); G[y].push_back(x); } dfs1(1,0); dfs2(1); GBT::init(); RT=GBT::bld(1,0); while(m--) { x=rd(),y=rd(); GBT::upd(x,y); printf("%d\n",GBT::tr[RT].f.mx()); } return 0; } ```