从染色谈全局平衡二叉树维护树链操作
这是一道很经典的树链剖分的题目,这里提出使用全局平衡二叉树的解法。
全局平衡二叉树处理树链问题是极具优势的,只需要做到 \(O(1)\) 子树打 tag,单点修改,下传标记,即可做到 \(O(\log n)\) 单次修改查询。
其用途显然不止优化 ddp。
全局平衡二叉树上,一条树链被如何表达?先让我们弄清楚如何表达一条树链。
设路径 \((u,v)\),\(d_u\) 表示原树深度,\(dep_u\) 表示在重构后的树上的深度。
-
当 \(u,v\) 不在一条重链上时
显然不断往上跳,以一端距离,一条从下往上的链可以被如下表达:
- 设 \(x=u\)
- 当 \(d_u\le d_x\),将当前的链接上 \(lik_{lc_u}+u\),其中 \(lik_x=lik_{lc_x}+x+lik_{rc_x}\),也即这一棵BST的中序遍历序列。
- 若 \(fa_u\) 处于另一条重链,令 \(x=fa_u\)
- 令 \(u=fa_u\),回到第二步。
x=u,y=v; while(Rt[u]!=Rt[v]){//重链这颗BST的树根编号 if(dep[Rt[u]]>dep[Rt[v]]){//树根在重构树上的深度,先跳深的 if(d[u]<=d[x]){ L=L+val[u]+s[lc[u]]; } if(lc[lt[u]]!=u&&rc[lt[u]]!=u)x=lt[u];//lt 是父亲的含义 u=lt[u]; } else { if(d[v]<=d[y]){ R=R+val[v]+s[lc[v]]; } if(lc[lt[v]]!=v&&rc[lt[v]]!=v)y=lt[v]; v=lt[v]; } }
-
当 \(u,v\) 在一条重链上
不妨设 \(dep_u<dep_v\),我们希望处理出它们的中间部分,并接上之前的两条向上的链。(这里如果对顺序有要求,可以顺带维护向下的链信息,这里直接用 \(rev\) 表示翻转了)。
此刻在同一颗 BST 上,则设 \(x=u,y=v\),开始不断向上跳,借助 \(d\) 的信息决定要不要加入这个子树。
u=x,v=y; while(x!=y){ if(dep[x]>dep[y]){ if(d[x]>=d[u])L=L+val[x]+s[rc[x]].rev(); x=lt[x]; } else { if(d[y]<=d[v])R=R+val[y]+s[lc[y]]; y=lt[y]; } } return L+val[x]+R.rev();//漏了最终这个点
那么查询就这样解决了(可能需要特别处理空链的合并),只需要维护子树内按中序遍历的信息总和即可,有时候需要维护翻转过来的信息。
现在考虑修改。
一条链还是向上面一样表达,唯一需要修改的是,当最后跳到一起,修改之后,需要不断向上跳到整颗 gbst 的根来 pushup
。
在实现细节上,注意先更改子树,打标记,再改这个点,然后 pushup
。
void change(int u,int v,int k){
int x=u,y=v;
while(Rt[u]!=Rt[v]){
if(dep[Rt[u]]>dep[Rt[v]]){
if(d[u]<=d[x])chg(lc[u],k),upd(u,k);
if(lc[lt[u]]!=u&&rc[lt[u]]!=u)x=lt[u];
u=lt[u];//upd内含pushup
}
else {
if(d[v]<=d[y])chg(lc[v],k),upd(v,k);
if(lc[lt[v]]!=v&&rc[lt[v]]!=v)y=lt[v];
v=lt[v];
}
pushup(u);pushup(v);
}
if(d[x]>d[y])swap(x,y);
u=x,v=y;
while(x!=y){
if(dep[x]>dep[y]){
if(d[x]>=d[u])chg(rc[x],k),upd(x,k);
x=lt[x];
}
else {
if(d[y]<=d[v])chg(lc[y],k),upd(y,k);
y=lt[y];
}
pushup(x);pushup(y);
}
upd(x,k);
while(x){
pushup(x);x=lt[x];
}
}
懒标记如何处理。
正常的写法是,对于查询或修改路径 \((u,v)\),先拿出两个点到整个 gbst 的树根路上的所有点,按 \(d\) 的顺序下传懒标记,这一步 \(O(\log n)\)。接着再进行正常的操作。
标记永久化的写法支持自底向上更新信息,查询时再往上跳遇到标记统计贡献即可。
可以看到,整个查询或修改都是 \(O(\log n)\) 的。
总复杂度 \(O((n+q)\log n)\)。