树链剖分学习笔记

树链剖分,简称树剖,用来解决一类维护静态树上路径信息的问题。就是对树进行一些操作,使其变成线性的结构,从而用线段树进行维护。

树链剖分的方法是轻重边剖分。我们将树上的边分成两种——轻边和重边。如果我们记siz[u]表示节点u的子树大小,在u的所有儿子之中,节点v的siz最大,则将边(u,v)称为重边,v称为u的重儿子。u到其他儿子的边就是轻边,其他的儿子就是轻儿子。

对于轻重边有这样的性质:

  1.轻儿子的子树大小小于或等于其父亲子树大小的一半。即若(u,v)为轻边,满足siz[v] <= siz[u] / 2。

证明其实很显然,假设存在一个轻儿子x使得siz[x] > siz[u] / 2,则u其他儿子siz之和都小于siz[x],那么x必然是重儿子,与假设矛盾,因此原命题成立。

  2.从根节点到某个节点v路径上的轻边个数不多于O(logn)。

证明:显然如果是叶子结点可以满足边的数量最多,假设有一个叶子结点v满足路径上的边均为轻边,而由上一条性质可知,每次经过轻边实际上就是子树规模减少了一半,因此至多经过O(logn)条轻边就可以到达v。

  3.定义重路径为一条路径所有的边都为重边(特别地,一个点也是一条重路径)。则对于每个节点到根节点的路径上都有不超过O(logn)条轻边和O(logn)条重路径。这个由性质2显然可以得到证明,因为轻边已经不超过O(logn),除了轻边都是重路径,因此也不超过O(logn)。

由以上三条性质可知,假如我们实现了对树进行轻重边剖分,那么每次处理路径复杂度都是O(logn)的,显然复杂度非常优秀。

现在我们就需要考虑怎么实现轻重边剖分了。

定义变量:

链前存图:head[N],nxt[M],to[M],N为节点个数,M为边的数量。

线段树相关:tree[N * 4],lazy[N * 4]

树上信息:

  dep[N]维护节点到根节点距离。

  fa[N]维护节点父亲。

  siz[N]维护节点大小。

  son[N]维护节点重儿子编号。

  top[N]维护一条重路径上dep值最小的点的编号。

  dfn[N],pos[N]维护节点dfs序,dfn[x]为dfs序为x的节点编号,pos[u]为节点u对应的dfs序编号,二者互相对应。

  til[N]维护dfs序上子树大小,[ pos[u],til[u] ]即一段存储u子树的序列。

好的相关变量定义完了(为什么这么多qwq)。

首先我们需要进行一些dfs操作维护这些变量:

第一次dfs,维护每个节点的父亲,深度,子树大小,重儿子编号。这个操作十分显然。

void dfs1(int u)
{
    dep[u] = dep[fa[u]] + 1;
    siz[u] = 1;
    for(int i = head[u];i;i = nxt[i])
    {
        int v = to[i];
        if(v == fa[u]) continue;
        fa[v] = u;
        dfs1(v);
        siz[u] += siz[v];
        if(siz[v] > siz[son[u]]) son[u] = v;//当一个儿子的siz比当前重儿子大就更新重儿子。
    }
    return;
}

第二次dfs,维护重路径上的top,dfs序,子树序列。这里一定要优先遍历重儿子,使得重路径在线段树中的位置是连续的。

void dfs2(int u)
{
    if(u == son[fa[u]]) top[u] = top[fa[u]];
    else top[u] = u;
    dfn[++tot] = u;
    pos[u] = tot;
    if(son[u]) dfs2(son[u]);
    for(int i = head[u];i;i = nxt[i])
    {
        int v = to[i];
        if(v == son[u] || v == fa[u]) continue;
        dfs2(v);
    }
    til[u] = tot;
    return;
}

这样两次dfs我们就成功维护好了重路径和树上信息了。然后就要进行路径修改操作了,通过递归实现。

如果两个节点就在同一条重路径上,那么线段树中这两个点之间的区间也是连续的,直接修改就行。如果不在同一路径上,那么就需要进行“跳”的操作,也就是将深度较大的点向上跳,直接跳到当前重路径的顶端,然后继续进行这个操作直到两节点位于同一重路径。我们发现最坏情况下就是两个节点都需要跳到根节点处,而由前面论证可知这样跳的次数不会超过O(logn)次,每次进行一次线段树修改,复杂度O(logn),因此进行一次路径修改的总复杂度为O(log²n)。

void path_change(int u,int v,int k)
{
    if(top[u] == top[v]) 
    {
        if(pos[u] > pos[v]) swap(u,v);
        modify(1,1,n,pos[u],pos[v],k);
        return;
    }
    if(dep[top[u]] > dep[top[v]]) swap(u,v);
    modify(1,1,n,pos[top[v]],pos[v],k);
    path_change(u,fa[top[v]],k);
    return;
}

 然后是路径查询,其实同理,只需要将路径修改中的修改部分改成查询就行,其实就是查询路径分成多条重路径上的答案然后求和。复杂度显然也是O(log²n)的。

int path_query(int u,int v)
{
    if(top[u] == top[v]) 
    {
        if(pos[u] > pos[v]) swap(u,v);
        return query(1,1,n,pos[u],pos[v]) % mod;
    }
    if(dep[top[u]] > dep[top[v]]) swap(u,v);
    return (query(1,1,n,pos[top[v]],pos[v]) + path_query(u,fa[top[v]])) % mod;
}

至于对子树进行修改,查询,只需要对[pos[x],til[x]]进行常规线段树修改查询操作就行了。

贴一个洛谷P3384树链剖分的完整代码:

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<ctime>
#include<cstdlib>
#include<set>
#include<queue>
#include<vector>
#include<string>
using namespace std;

#define P system("pause");
#define A(x) cout << #x << " " << (x) << endl;
#define AA(x,y) cout << #x << " " << (x) << #y << " " << (y) << endl;
#define ll long long
#define inf 1000000000
#define linf 10000000000000000
#define mem(x) memset(x,0,sizeof(x))

int read()
{
    int x = 0,f = 1;
    char c = getchar();
    while(c < '0' || c > '9')
    {
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9')
    {
        x = (x << 3) + (x << 1) + c - '0';
        c = getchar();
    }
    return f * x;
}

#define ls (x << 1),l,mid
#define rs (x << 1 | 1),mid + 1,r
#define mid ((l + r) >> 1)
#define N 1000010
#define M N << 1
int tree[N << 2],lazy[N << 2],mod;
int n,m,r,cnt;
int head[N],nxt[M],to[M],val[N],fa[N],dep[N],siz[N],son[N],top[N];
int dfn[N],pos[N],til[N],tot;
void push_up(int x)
{
    tree[x] = (tree[x << 1] + tree[x << 1 | 1]) % mod;
    return;
}
void build(int x,int l,int r)
{
    if(l == r)
    {
        tree[x] = val[dfn[l]] % mod;
        return;
    }
    build(ls);
    build(rs);
    push_up(x);
    return;
}
void Add(int x,int l,int r,int k)
{
    tree[x] = (1ll * tree[x] + (r - l + 1) * k) % mod;
    lazy[x] = (lazy[x] + k) % mod;
    return;
}
void push_down(int x,int l,int r)
{
    if(!lazy[x]) return;
    Add(ls,lazy[x]);
    Add(rs,lazy[x]);
    lazy[x] = 0;
    return;
}
void modify(int x,int l,int r,int p,int q,int k)
{
    if(p <= l && r <= q)
    {
        Add(x,l,r,k);
        return;
    }
    push_down(x,l,r);
    if(p <= mid) modify(ls,p,q,k);
    if(q > mid) modify(rs,p,q,k);
    push_up(x);
    return;
}
int query(int x,int l,int r,int p,int q)
{
    if(p <= l && r <= q) return tree[x] % mod;
    int ret = 0;
    push_down(x,l,r);
    if(p <= mid) ret = (ret + query(ls,p,q)) % mod;
    if(q > mid) ret = (ret + query(rs,p,q)) % mod;
    return ret % mod;    
}
void add(int u,int v)
{
    nxt[++cnt] = head[u];
    head[u] = cnt;
    to[cnt] = v;
}
void dfs1(int u)
{
    dep[u] = dep[fa[u]] + 1;
    siz[u] = 1;
    for(int i = head[u];i;i = nxt[i])
    {
        int v = to[i];
        if(v == fa[u]) continue;
        fa[v] = u;
        dfs1(v);
        siz[u] += siz[v];
        if(siz[v] > siz[son[u]]) son[u] = v;
    }
    return;
}
void dfs2(int u)
{
    if(u == son[fa[u]]) top[u] = top[fa[u]];
    else top[u] = u;
    dfn[++tot] = u;
    pos[u] = tot;
    if(son[u]) dfs2(son[u]);
    for(int i = head[u];i;i = nxt[i])
    {
        int v = to[i];
        if(v == son[u] || v == fa[u]) continue;
        dfs2(v);
    }
    til[u] = tot;
    return;
}
void path_change(int u,int v,int k)
{
    if(top[u] == top[v]) 
    {
        if(pos[u] > pos[v]) swap(u,v);
        modify(1,1,n,pos[u],pos[v],k);
        return;
    }
    if(dep[top[u]] > dep[top[v]]) swap(u,v);
    modify(1,1,n,pos[top[v]],pos[v],k);
    path_change(u,fa[top[v]],k);
    return;
}
int path_query(int u,int v)
{
    if(top[u] == top[v]) 
    {
        if(pos[u] > pos[v]) swap(u,v);
        return query(1,1,n,pos[u],pos[v]) % mod;
    }
    if(dep[top[u]] > dep[top[v]]) swap(u,v);
    return (query(1,1,n,pos[top[v]],pos[v]) + path_query(u,fa[top[v]])) % mod;
}
int main()
{
    n = read(),m = read(),r = read(),mod = read();
    for(int i = 1;i <= n;i++) val[i] = read();
    for(int i = 1,u,v;i < n;i++)
    {
        u = read(),v = read();
        add(u,v);
        add(v,u);
    }
    dfs1(r);
    dfs2(r);
    build(1,1,n);
    int op,x,y,z;
    while(m--)
    {
        op = read(),x = read();
        switch(op)
        {
            case 1:
                y = read(),z = read();
                path_change(x,y,z);
                break;
            case 2:
                y = read();
                printf("%d
",path_query(x,y) % mod);
                break;
            case 3:
                z = read();
                modify(1,1,n,pos[x],til[x],z);
                break;
            case 4:
                printf("%d
",query(1,1,n,pos[x],til[x]) % mod);
                break;
        }
    }
    return 0;
}
P3384完整代码

好的树链剖分常用操作就完了,就是将树形结构变成线性结构处理,非常便捷,虽然码量不小,但其实并不难写。

 

原文地址:https://www.cnblogs.com/lijilai-oi/p/11261006.html