树链剖分小结

Posted clno1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树链剖分小结相关的知识,希望对你有一定的参考价值。

 最近在学树链剖分,写篇文章记录一下。

原理及代码:https://blog.csdn.net/cdy1206473601/article/details/79189553#_11

题目:https://www.cnblogs.com/hanruyun/p/9577500.html

 

树链剖分说白了就是把一棵树拆成若干个不相交的链,然后用一些数据结构去维护这些链。因为通常的数据结构处理区间信息很容易,但处理树上的信息就显得捉襟见肘了。于是我们想到把树拍成一个区间用线段树去维护信息。(和树的dfs序是类似的原理)。

 树链剖分的几个常见应用:

①查询/修改树的子树的值:因为dfs的遍历顺序关系,每颗子树在线段树上必定是一段连续的区间,所以容易处理。

②查询/修改树两点路径间的值:因为每条重链是一段连续区间,那么两点间路径肯定是若干条链合起来得到,也就是线段树上的几段区间。

 

模板题:洛谷P3384

#pragma comment(linker,"/STACK:102400000,102400000")
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,rt,MOD,num=0,v[N];
struct node{
    int dep,fa,val,sz,heavy;
    int toseg,top; 
}tree[N];  //原本的树 
int totree[N<<2];  //线段树点i代表原树的点totree[i] 

/*-------------------------以下为线段树-----------------------------*/
int sum[N<<2],tag[N<<2];
void pushdown(int rt,int len1,int len2) {
    sum[rt<<1]+=tag[rt]*len1%MOD; sum[rt<<1]%=MOD; 
    tag[rt<<1]+=tag[rt]%MOD; tag[rt<<1]%=MOD;
    sum[rt<<1|1]+=tag[rt]*len2%MOD; sum[rt<<1|1]%=MOD;
    tag[rt<<1|1]+=tag[rt]%MOD; tag[rt<<1|1]%=MOD;
    tag[rt]=0;
}
void pushup(int rt) {
    sum[rt]=(sum[rt<<1]+sum[rt<<1|1])%MOD;
}

void build(int rt,int l,int r) {
    if (l==r) {
        sum[rt]=tag[rt]=tree[totree[l]].val%MOD;
        return;
    }
    int mid=l+r>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    pushup(rt);
}

void update(int rt,int l,int r,int ql,int qr,int v) {
    if (ql<=l && r<=qr) {
        sum[rt]+=v*(r-l+1)%MOD; sum[rt]%=MOD;
        tag[rt]+=v%MOD; tag[rt]%=MOD;
        return;
    }
    int mid=l+r>>1;
    pushdown(rt,mid-l+1,r-mid);
    if (ql<=mid) update(rt<<1,l,mid,ql,qr,v);
    if (qr>mid) update(rt<<1|1,mid+1,r,ql,qr,v);
    pushup(rt);
}

int query(int rt,int l,int r,int ql,int qr) {
    if (ql<=l && r<=qr) return sum[rt];
    int mid=l+r>>1,ret=0;
    pushdown(rt,mid-l+1,r-mid);
    if (ql<=mid) ret=(ret+query(rt<<1,l,mid,ql,qr))%MOD;
    if (qr>mid) ret=(ret+query(rt<<1|1,mid+1,r,ql,qr))%MOD;
    return ret;
}

/*--------------------------以下为树链剖分----------------------------*/
int cnt=1,head[N<<1],to[N<<1],nxt[N<<1];
void add_edge(int x,int y) {
    nxt[++cnt]=head[x]; to[cnt]=y; head[x]=cnt;
}

void dfs1(int x,int fa,int dep) {  //点x的父亲为fa深度为dep 
    tree[x].dep=dep;
    tree[x].fa=fa;
    tree[x].sz=1;
    tree[x].val=v[x];
    int maxson=-1;
    for (int i=head[x];i;i=nxt[i]) {
        int y=to[i];
        if (y==fa) continue;
        dfs1(y,x,dep+1);
        tree[x].sz+=tree[y].sz;
        if (tree[y].sz>maxson) tree[x].heavy=y,maxson=tree[y].sz;
    }
}

void dfs2(int x,int top) {  //点x所在树链的top 
    tree[x].toseg=++num;
    tree[x].top=top;
    totree[num]=x;
    if (!tree[x].heavy) return;  //叶子结点 
    dfs2(tree[x].heavy,top);  //先剖分重儿子 
    for (int i=head[x];i;i=nxt[i]) {  //再剖分轻儿子 
        int y=to[i];
        if (y==tree[x].fa || y==tree[x].heavy) continue;
        dfs2(y,y);
    }
}

//以下两个函数是树链剖分的精髓 
void update2(int x,int y,int z) {  //修改x到y路径的值 
    while (tree[x].top!=tree[y].top) {  //不在同一条链上 
        if (tree[tree[x].top].dep<tree[tree[y].top].dep) swap(x,y);  //x为深度大的链 
        update(1,1,n,tree[tree[x].top].toseg,tree[x].toseg,z);  //x向上跳的同时更新 
        x=tree[tree[x].top].fa;  //深度大的向上跳 
    }
    if (tree[x].dep>tree[y].dep) swap(x,y);  //这里x和y在同一条链 
    update(1,1,n,tree[x].toseg,tree[y].toseg,z);  //x和y这条链的更新 
}

int query2(int x,int y) {  //查询x到y路径的值,原理同上 
    int ret=0;
    while (tree[x].top!=tree[y].top) {
        if (tree[tree[x].top].dep<tree[tree[y].top].dep) swap(x,y);
        ret=(ret+query(1,1,n,tree[tree[x].top].toseg,tree[x].toseg))%MOD;
        x=tree[tree[x].top].fa;
    }
    if (tree[x].dep>tree[y].dep) swap(x,y);
    ret=(ret+query(1,1,n,tree[x].toseg,tree[y].toseg))%MOD;
    return ret;
}

int main()
{
    cin>>n>>m>>rt>>MOD;
    for (int i=1;i<=n;i++) scanf("%d",&v[i]);
    for (int i=1;i<n;i++) {
        int x,y; scanf("%d%d",&x,&y);
        add_edge(x,y); add_edge(y,x);
    }
    dfs1(rt,0,1);  //树链剖分准备信息 
    dfs2(rt,rt);  //开始树链剖分 
    build(1,1,n);  //把树链建成线段树 
    for (int i=1;i<=m;i++) {
        int opt,x,y,z; scanf("%d",&opt);
        if (opt==1) {  //修改x到y路径的点的值 
            scanf("%d%d%d",&x,&y,&z);
            update2(x,y,z%MOD);
        }
        if (opt==2) {  //查询x到y路径的值 
            scanf("%d%d",&x,&y);
            printf("%d\\n",query2(x,y));
        }
        if (opt==3) {  //修改x子树的值 
            scanf("%d%d",&x,&z);
            update(1,1,n,tree[x].toseg,tree[x].toseg+tree[x].sz-1,z%MOD);
        }
        if (opt==4) {  //查询x子树的值 
            scanf("%d",&x);
            printf("%d\\n",query(1,1,n,tree[x].toseg,tree[x].toseg+tree[x].sz-1));
        }
    }
    return 0;
}

 

比较简单的树链剖分题目其实就是上面说的这两种应用或其变式,都是对树链剖分本身基本没有变化,都是在维护树链的数据结构做变化 (例如:线段树等等)。这里还有几道练习题:

洛谷P4116 Qtree3

树链剖分后,线段树维护区间黑点的深度最小点(即区间黑点中深度最小的一个),那么第一个操作就是单点更新,第二个操作就是区间查询而已。变成了简单的线段树操作。

技术图片
#pragma comment(linker,"/STACK:102400000,102400000")
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int INF=0x3f3f3f3f;
typedef long long LL;
int n,m,rt,num=0,v[N];
struct node{
    int dep,fa,val,sz,heavy;
    int toseg,top; 
}tree[N];  //原本的树 
int totree[N<<2];  //线段树点i代表原树的点totree[i] 

/*-------------------------以下为线段树-----------------------------*/
LL Min[N<<2];
void pushup(int rt) {
    if (tree[Min[rt<<1]].dep<tree[Min[rt<<1|1]].dep) Min[rt]=Min[rt<<1];
    else Min[rt]=Min[rt<<1|1];
}

void build(int rt,int l,int r) {
    Min[rt]=0;
    if (l==r) return;
    int mid=l+r>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
}

void update(int rt,int l,int r,int ql,int qr,int v) {
    if (ql<=l && r<=qr) {
        Min[rt]=v;
        return;
    }
    int mid=l+r>>1;
    if (ql<=mid) update(rt<<1,l,mid,ql,qr,v);
    if (qr>mid) update(rt<<1|1,mid+1,r,ql,qr,v);
    pushup(rt);
}

int query(int rt,int l,int r,int ql,int qr) {
    if (ql<=l && r<=qr) return Min[rt];
    int mid=l+r>>1,ret=0;
    if (ql<=mid) {
        int t=query(rt<<1,l,mid,ql,qr);
        if (tree[ret].dep>tree[t].dep) ret=t;
    }
    if (qr>mid) {
        int t=query(rt<<1|1,mid+1,r,ql,qr);
        if (tree[ret].dep>tree[t].dep) ret=t;
    }
    return ret;
}

/*--------------------------以下为树链剖分----------------------------*/
int cnt=1,head[N<<1],to[N<<1],nxt[N<<1];
void add_edge(int x,int y) {
    nxt[++cnt]=head[x]; to[cnt]=y; head[x]=cnt;
}

void dfs1(int x,int fa,int dep) {  //点x的父亲为fa深度为dep 
    tree[x].dep=dep;
    tree[x].fa=fa;
    tree[x].sz=1;
    tree[x].val=v[x];
    int maxson=-1;
    for (int i=head[x];i;i=nxt[i]) {
        int y=to[i];
        if (y==fa) continue;
        dfs1(y,x,dep+1);
        tree[x].sz+=tree[y].sz;
        if (tree[y].sz>maxson) tree[x].heavy=y,maxson=tree[y].sz;
    }
}

void dfs2(int x,int top) {  //点x所在树链的top 
    tree[x].toseg=++num;
    tree[x].top=top;
    totree[num]=x;
    if (!tree[x].heavy) return;  //叶子结点 
    dfs2(tree[x].heavy,top);  //先剖分重儿子 
    for (int i=head[x];i;i=nxt[i]) {  //再剖分轻儿子 
        int y=to[i];
        if (y==tree[x].fa || y==tree[x].heavy) continue;
        dfs2(y,y);
    }
}

int query2(int x,int y) {  //查询x到y路径的值,原理同上 
    int ret=0;
    while (tree[x].top!=tree[y].top) {
        if (tree[tree[x].top].dep<tree[tree[y].top].dep) swap(x,y);
        int t=query(1,1,n,tree[tree[x].top].toseg,tree[x].toseg);
        if (tree[ret].dep>tree[t].dep) ret=t;
        x=tree[tree[x].top].fa;
    }
    if (tree[x].dep>tree[y].dep) swap(x,y);
    int t=query(1,1,n,tree[x].toseg,tree[y].toseg);
    if (tree[ret].dep>tree[t].dep) ret=t;
    return ret;
}

int main()
{
    cin>>n>>m;
    for (int i=1;i<n;i++) {
        int x,y; scanf("%d%d",&x,&y);
        add_edge(x,y); add_edge(y,x);
    }
    rt=1;
    dfs1(rt,0,1);  //树链剖分准备信息 
    dfs2(rt,rt);  //开始树链剖分 
    build(1,1,n);  //把树链建成线段树 

    tree[0].dep=INF;
    for (int i=1;i<=m;i++) {
        int opt,x; scanf("%d%d",&opt,&x);
        if (opt==0) {
            if (v[x]==0) update(1,1,n,tree[x].toseg,tree[x].toseg,x);
            if (v[x]==1) update(1,1,n,tree[x].toseg,tree[x].toseg,0);
            v[x]=1-v[x];
        } else {
            int t=query2(1,x);
            if (t==0) puts("-1");
            else printf("%d\\n",t);
        }
    }
    return 0;
}
View Code

 

洛谷P4092 HEOI2016/TJOI2016 树

这道题和上题差不多,唯一变化只是这里要维护的是区间标记点深度最大点。

技术图片
#pragma comment(linker,"/STACK:102400000,102400000")
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int INF=0x3f3f3f3f;
typedef long long LL;
int n,m,rt,num=0,v[N];
struct node{
    int dep,fa,val,sz,heavy;
    int toseg,top; 
}tree[N];  //原本的树 
int totree[N<<2];  //线段树点i代表原树的点totree[i] 

/*-------------------------以下为线段树-----------------------------*/
LL Max[N<<2];
void pushup(int rt) {
    if (tree[Max[rt<<1]].dep>tree[Max[rt<<1|1]].dep) Max[rt]=Max[rt<<1];
    else Max[rt]=Max[rt<<1|1];
}

void build(int rt,int l,int r) {
    Max[rt]=0;
    if (l==r) return;
    int mid=l+r>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
}

void update(int rt,int l,int r,int ql,int qr,int v) {
    if (ql<=l && r<=qr) {
        Max[rt]=v;
        return;
    }
    int mid=l+r>>1;
    if (ql<=mid) update(rt<<1,l,mid,ql,qr,v);
    if (qr>mid) update(rt<<1|1,mid+1,r,ql,qr,v);
    pushup(rt);
}

int query(int rt,int l,int r,int ql,int qr) {
    if (ql<=l && r<=qr) return Max[rt];
    int mid=l+r>>1,ret=0;
    if (ql<=mid) {
        int t=query(rt<<1,l,mid,ql,qr);
        if (tree[ret].dep<tree[t].dep) ret=t;
    }
    if (qr>mid) {
        int t=query(rt<<1|1,mid+1,r,ql,qr);
        if (tree[ret].dep<tree[t].dep) ret=t;
    }
    return ret;
}

/*--------------------------以下为树链剖分----------------------------*/
int cnt=1,head[N<<1],to[N<<1],nxt[N<<1];
void add_edge(int x,int y) {
    nxt[++cnt]=head[x]; to[cnt]=y; head[x]=cnt;
}

void dfs1(int x,int fa,int dep) {  //点x的父亲为fa深度为dep 
    tree[x].dep=dep;
    tree[x].fa=fa;
    tree[x].sz=1;
    tree[x].val=v[x];
    int maxson=-1;
    for (int i=head[x];i;i=nxt[i]) {
        int y=to[i];
        if (y==fa) continue;
        dfs1(y,x,dep+1);
        tree[x].sz+=tree[y].sz;
        if (tree[y].sz>maxson) tree[x].heavy=y,maxson=tree[y].sz;
    }
}

void dfs2(int x,int top) {  //点x所在树链的top 
    tree[x].toseg=++num;
    tree[x].top=top;
    totree[num]=x;
    if (!tree[x].heavy) return;  //叶子结点 
    dfs2(tree[x].heavy,top);  //先剖分重儿子 
    for (int i=head[x];i;i=nxt[i]) {  //再剖分轻儿子 
        int y=to[i];
        if (y==tree[x].fa || y==tree[x].heavy) continue;
        dfs2(y,y);
    }
}

int query2(int x,int y) {  //查询x到y路径的值,原理同上 
    int ret=0;
    while (tree[x].top!=tree[y].top) {
        if (tree[tree[x].top].dep<tree[tree[y].top].dep) swap(x,y);
        int t=query(1,1,n,tree[tree[x].top].toseg,tree[x].toseg);
        if (tree[ret].dep<tree[t].dep) ret=t;
        x=tree[tree[x].top].fa;
    }
    if (tree[x].dep>tree[y].dep) swap(x,y);
    int t=query(1,1,n,tree[x].toseg,tree[y].toseg);
    if (tree[ret].dep<tree[t].dep) ret=t;
    return ret;
}

int main()
{
    cin>>n>>m;
    for (int i=1;i<n;i++) {
        int x,y; scanf("%d%d",&x,&y);
        add_edge(x,y); add_edge(y,x);
    }
    rt=1;
    dfs1(rt,0,1);  //树链剖分准备信息 
    dfs2(rt,rt);  //开始树链剖分 
    build(1,1,n);  //把树链建成线段树 

    tree[0].dep=0;
    update(1,1,n,tree[1].toseg,tree[1].toseg,1);
    for (int i=1;i<=m;i++) {
        char opt[3]; int x; scanf("%s%d",&opt,&x);
        if (opt[0]==C) {
            update(1,1,n,tree[x].toseg,tree[x].toseg,x);
        } else {
            int t=query2(1,x);
            printf("%d\\n",t);
        }
    }
    return 0;
}
View Code

 

以上是关于树链剖分小结的主要内容,如果未能解决你的问题,请参考以下文章

树链剖分小结

长链剖分小结

BZOJ 2243--染色(树链剖分)

树链剖分详解

树链剖分详解

树链剖分