【笔记】树链(熟练)剖分

/*参考 https://www.cnblogs.com/LoveYayoi/p/6745541.html

https://www.cnblogs.com/George1994/p/7821357.html

以及我校学长的优秀讲解。

*/

树链剖分可以将树剖分成一条一条的链,形成一条新链,然后通过一些维护线性数据的数据结构(比如线段树)来维护树上的信息,例如 树上求两点间边or点权和、边权or点权max or min、修改边or点权 等。

P.S.来自另一位大佬(LoveYayoi)的解释:给定一棵形态不变的树,要求支持查询一条链,修改一条链。

对于“给定一棵形态变化的树,要求支持查询和修改一条链”,需要用到LCT。

一、概念:

重儿子:u的所有子节点中,子树最大的一个。特别的,根节点自己是一个重儿子。

轻儿子:u的所有子节点中,除了重儿子的其他儿子。

重边:u与其重儿子所连的边。

轻边:u与其轻儿子所连的边。

重链:重边所形成的链。

轻链:轻边所形成的链。

dep[i]:点i在树上的深度。

siz[i]:以点i为根的子树(包括点i)的大小(点数之和)。

fa[i]:点i的父节点。

top[i]:点i所在的链的顶端。对于轻链,其顶端为点i本身。

dfn[i]:点i在DFS序中的编号。

rnk[i]:DFS中编号为i的点在树上的编号。

二、性质:

1.一棵树被剖分后所形成的重链数与轻链数不会超过logn条

->这样每蹦一次操作一次,共操作O(重链数)=O(logn)次;每次操作的复杂度是O(logn);

总共的时间复杂度是O(qlogn^2)

2.若一条边(u,v)是一条轻边,则size(v)<size(u)/2。

->若一条边(u,v)是一条轻边,即点v是轻儿子,则点u至少有两个子节点;若点v是轻儿子,其子节点最多不会超过size(u)/2,否则v就成了重儿子。

三、代码:

(一)预处理

第一遍DFS,预处理出dep[]、fa[]、siz[]。

void dfs1(int u,int f,int d){
    dep[u]=d;
    siz[u]=1;
    fa[u]=f;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]) continue;
        dfs1(v,u,d+1);
        siz[u]+=siz[v];
        if(!son[u]||siz[v]>siz[son[u]]) son[u]=v;
    }
}

第二遍DFS,预处理出tid[]、rnk[]、top[]。

int idx;
void dfs2(int u,int tp){
    dfn[u]=++idx;//u的DFS序编号为idx 
    rnk[idx]=u;//在DFS序上编号为idx的点在树上的编号为u 
    top[u]=tp;
    if(!son[u]) return;
    dfs2(son[u],tp);//处理重链 
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]||v==son[u]) continue;
        dfs2(v,v);//处理轻链 
    }
}

(二)树上路径问题

①LCA(u,v)

对于树上两点u与v间路径的一系列问题,可以将其拆分为u->lca(u,v)和v->lca(u,v)两个子问题。类似于倍增求LCA,树链剖分也是类似的方法。对于u和v两点的LCA,我们让所在链的深度(即dep[top[]])大的一点跳到另一点所在的链上,当两点同在一链时,深度小的一点就是LCA(u,v)

int lca(int u,int v){
    int f1=top[u],f2=top[v];
    //令所在链的深度大的一点跳到另一点所在的链上 
    while(f1!=f2){
        //令u为所在链的深度大的一点 
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v); 
        }
        u=fa[f1];
        //u跳到所在链的顶端f1=top[u]的父节点,从而进入下一条重链。 
        f1=top[u];
        //同样所在链顶端f1也要修改 
    }
    //当两点同处一链时,深度小的为LCA 
    int lca;
    if(dep[u]>=dep[v]) {
        lca=v;
    }
    else lca=u;
    return lca;
}

②链上点权和、点权max...

在求LCA时通过线段树随时更新ans即可。注意查询时查询的是点的DFS序

 ->链上点权和

int lca_sum(int u,int v){
    int ans=0;
    int f1=top[u],f2=top[v];
    while(f1!=f2){
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v);
        }
        ans+=ask_sum(1,dfn[f1],dfn[u]);//线段树区间求和 
        u=fa[f1];
        f1=top[u];
    }
    int lca,tmp;
    if(dep[u]>=dep[v]) {
        lca=v,tmp=u;
    }
    else lca=u,tmp=v;
    ans+=ask_sum(1,dfn[lca],dfn[tmp]);
    return ans;
}

->链上最大点权

int lca_max(int u,int v){
    int ans=-(1<<30);
    int f1=top[u],f2=top[v];
    while(f1!=f2){
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v);
        }
        ans=max(ans,ask_max(1,tid[f1],tid[u]));//线段树区间max 
        u=fa[f1];
        f1=top[u];
    }
    int lca,tmp;
    if(dep[u]>=dep[v]) {
        lca=v,tmp=u;
    }
    else lca=u,tmp=v;
    ans=max(ans,ask_max(1,tid[lca],tid[tmp]));
    return ans;
}

P.S.

1.求链上边权和、链上边权max...时,可以将边权下放为点权。此时注意查询的边界问题:由于u向上蹦时,会从u蹦到fa[top[u]]。事实上我们只经过了top[u]~u之间的边,因此查询的边界为dfn[top[u]]~dfn[u],而不包括dfn[fa[top[u]]]的信息,因为没有经过其保存的边。

 2.建树时,已知的是线段树上的编号,即DFS序编号。所以需要将线段树编号转化为树上的编号,例如tree[u].sum=val[rnk[l]],其中val[]表示点权,rnk[l]表示DFS编号为l的点在树上的编号为dfn[l]。

 

猜你喜欢

转载自www.cnblogs.com/Loi-Brilliant/p/9126264.html