从染色谈全局平衡二叉树维护树链操作

染色

这是一道很经典的树链剖分的题目,这里提出使用全局平衡二叉树的解法。

全局平衡二叉树处理树链问题是极具优势的,只需要做到 \(O(1)\) 子树打 tag,单点修改,下传标记,即可做到 \(O(\log n)\) 单次修改查询。

其用途显然不止优化 ddp。

全局平衡二叉树上,一条树链被如何表达?先让我们弄清楚如何表达一条树链。

设路径 \((u,v)\)\(d_u\) 表示原树深度,\(dep_u\) 表示在重构后的树上的深度。

  1. \(u,v\) 不在一条重链上时

    显然不断往上跳,以一端距离,一条从下往上的链可以被如下表达:

    1. \(x=u\)
    2. \(d_u\le d_x\),将当前的链接上 \(lik_{lc_u}+u\),其中 \(lik_x=lik_{lc_x}+x+lik_{rc_x}\),也即这一棵BST的中序遍历序列。
    3. \(fa_u\) 处于另一条重链,令 \(x=fa_u\)
    4. \(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];
           }
       }
    
  2. \(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)\)

code

posted @ 2025-08-13 15:16  spdarkle  阅读(38)  评论(1)    收藏  举报