首页 > 其他分享 >[OI] 树链剖分

[OI] 树链剖分

时间:2024-10-05 17:33:41浏览次数:3  
标签:剖分 OI int 树链 ask 节点 now id size

学的时候比较朦胧,现在不朦胧了,所以写一下

讲解

重儿子:一个节点的子树大小最大的儿子
轻儿子:非重儿子
重链:节点 -> 重儿子 -> 重儿子 .. 这样的链

A beautiful Tree

蓝线为重链

可以发现,树上的所有节点一定属于且仅属于一个重链

首先要知道如何找重链

这很简单,可以通过一遍 DFS 得到:

void dfs(int now){
    size[now]=1;
    int maxsonsize=0;
    for(i:遍历所有子节点){
        dfs(i)
        if(size[i]>maxsonsize){
            maxson[now]=i;
            maxsonsize=size[i]
        }
        size[now]+=size[i]
    }
}

其中 size 是节点的子树大小

为什么一定要剖出重链来?因为我们要进行的是在链上跳跃的操作,而我们可以跳跃的范围是一整条链,因此链越长,对复杂度就越有利,而且我们将不同的重链剖出来,还能保证每一个节点都在一条重链上,不重不漏

找出重儿子以后怎么找重链呢

这个就更简单了,我们再做一遍 DFS,记录每个链顶端的节点,然后将其赋给链中的每一个节点(或者你在这里开个 cnt 也是可以的,只要能起到区分的作用就行),这样,值相同的节点就一定在同一条链里了

void dfs2(int now,int topnode){
    top[now]=topnode;
    if(maxson[now]==0) return; //没有儿子就返回
    dfs2(maxson[now],top_node) //搜索重儿子,此时不改变链
    for(i:遍历子节点){
        if(i!=maxson[now]){
            dfs2(i,i);        //轻儿子的重链顶端就是这个轻儿子,可以看上面的图
        }                      //如果你在这里写 cnt 的话就是 ++cnt
    }
}

实际上我们还需要在这两遍 DFS 中维护一些信息,具体的信息列在下面:

DFS1

  • 节点父亲
  • 节点深度
  • 节点子树大小
  • 节点的重儿子编号

DFS2

  • 构建链
  • 按遍历顺序为每个节点分配新编号
  • 将原节点权值迁移到新编号

可以写出下面两个代码:

void dfs1(int now,int last){
    fa[now]=last;
    deep[now]=deep[last]+1;
    size[now]=1;
    int maxsonsize=0;
    for(int i:e[now]){
        if(i!=last){
            dfs1(i,now);
            if(size[i]>maxsonsize){
                maxson[now]=i;
                maxsonsize=size[i];
            }
            size[now]+=size[i];
        }
    }
}
void dfs2(int now,int nowtop,int last){
    id[now]=++cnt;
    wnew[id[now]]=w[now];
    top[now]=nowtop;
    if(!maxson[now]) return;
    dfs2(maxson[now],nowtop,now);
    for(int i:e[now]){
        if(i!=last and i!=maxson[now]){
            dfs2(i,i,now);
        }
    }
}

这里我们给每个节点都分配了新的编号,那么有什么用吗

因为我们这么分配编号有两个非常好的性质

  • 同一个重链上的点,编号总是连续的,并且上面的节点编号总是比下面的节点编号要小

  • 同一个子树中的点,编号是一个连续区间,并且这个区间总是 \([id_r,id_r+size-1]\)(\(r\) 是子树根节点)

但是需要注意的是,为了实现这两个非常好的性质,我们需要在 DFS2 中优先遍历重儿子,因为重儿子和当前节点在一条链中,只有优先遍历了重儿子,才能保证按遍历顺序分配的编号是连续的

那么有了这两个非常好的性质,我们可以干什么呢

  • 查询路径信息

假如有一道题让我们查询树上 \((x,y)\) 的简单路径权值和(点权)

那么我们可以考虑这样降低复杂度:

  • 如果 \(x,y\) 不在一条链上,将其中链顶深度较小的那个节点跳到它所在的链顶,同时统计该节点到其顶端的答案
  • 重复如上操作,直到 \(x,y\) 在一条链上
  • 此时直接统计即可

以上操作中,由于我们只在同一条链上跳,因此编号总是连续的,所以可以用数据结构来维护

下面是一份线段树维护的查询

int ask_path_sum(int x,int y){
    int res=0;
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        res+=ask_sum(1,id[top[x]],id[x]);
        x=fa[top[x]];
    }
    if(deep[x]<deep[y]) swap(x,y);
    res+=ask_sum(1,id[y],id[x]);
    return res;
}

路径修改同理

然后考虑怎么用第二个性质

第二个性质也非常好,可以用来作子树整体修改/查询

int ask_subtree(int x){
    return stree::ask_sum(1,id[x],id[x]+size[x]-1);
}

例题

树的统计

  • 单点修改
  • 路径和查询
  • 路径最值查询

这两个信息都能用线段树来维护

单点修改总是简单的,直接在线段树上定位即可

#include<bits/stdc++.h>
using namespace std;
#define int long long
int deep[200001],fa[200001],size[200001],maxson[200001];
vector<int>e[200001];
void dfs1(int now,int last){
    fa[now]=last;
    deep[now]=deep[last]+1;
    size[now]=1;
    int maxsonsize=0;
    for(int i:e[now]){
        if(i!=last){
            dfs1(i,now);
            if(size[i]>maxsonsize){
                maxson[now]=i;
                maxsonsize=size[i];
            }
            size[now]+=size[i];
        }
    }
}
int w[200001];
int id[200001],top[200001],wnew[200001];
int cnt=0;
void dfs2(int now,int nowtop,int last){
    id[now]=++cnt;
    wnew[id[now]]=w[now];
    top[now]=nowtop;
    if(!maxson[now]) return;
    dfs2(maxson[now],nowtop,now);
    for(int i:e[now]){
        if(i!=last and i!=maxson[now]){
            dfs2(i,i,now);
        }
    }
}
namespace stree{
    struct tree{
        int l,r;
        int sum,max;
    }t[800001];
    #define tol (id*2)
    #define tor (id*2+1)
    #define mid(l,r) mid=((l)+(r))/2
    void build(int id,int l,int r){
        t[id].l=l;t[id].r=r;
        if(l==r){
            t[id].sum=wnew[l];
            t[id].max=wnew[l];
            return;
        }
        int mid(l,r);
        build(tol,l,mid);
        build(tor,mid+1,r);
        t[id].sum=(t[tol].sum+t[tor].sum);
        t[id].max=max(t[tol].max,t[tor].max);
    }
    int ask_sum(int id,int l,int r){
        if(l>r) swap(l,r);
        if(l<=t[id].l and t[id].r<=r){
            return t[id].sum;
        }
        pushdown(id);
        if(r<=t[tol].r) return ask_sum(tol,l,r);
        else if(l>=t[tor].l) return ask_sum(tor,l,r);
        else{
            return (ask_sum(tol,l,t[tol].r)+ask_sum(tor,t[tor].l,r));
        }
    }
    int ask_max(int id,int l,int r){
        if(l>r) swap(l,r);
        if(l<=t[id].l and t[id].r<=r){
            return t[id].max;
        }
        pushdown(id);
        if(r<=t[tol].r) return ask_max(tol,l,r);
        else if(l>=t[tor].l) return ask_max(tor,l,r);
        else{
            return max(ask_max(tol,l,t[tol].r),ask_max(tor,t[tor].l,r));
        }
    }
}
int ask_path_max(int x,int y){
    int res=-1;
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        res=max(res,stree::ask_max(1,id[top[x]],id[x]));
        x=fa[top[x]];
    }
    if(deep[x]<deep[y]) swap(x,y);
    res=max(res,stree::ask_max(1,id[y],id[x]));
    return res;
}
int ask_path_sum(int x,int y){
    int res=0;
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        res+=stree::ask_sum(1,id[top[x]],id[x]);
        x=fa[top[x]];
    }
    if(deep[x]<deep[y]) swap(x,y);
    res+=stree::ask_sum(1,id[y],id[x]);
    return res;
}
int n,m;
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n-1;++i){
        int x,y;
        scanf("%lld %lld",&x,&y);
        e[x].push_back(y);
        e[y].push_back(x);
    }
    for(int i=1;i<=n;++i){
        scanf("%lld",&w[i]);
    }
    scanf("%lld",&m);
    dfs1(1,0);
    dfs2(1,1,0);
    stree::build(1,1,n);
    while(m--){
        string op;int x,y,z;cin>>op;
        if(op[0]=='C'){
            scanf("%lld %lld",&x,&z);
            stree::change(1,id[x],id[x],z-stree::ask_sum(1,id[x],id[x]));
        }
        if(op[0]=='Q' and op[1]=='M'){
            scanf("%lld %lld",&x,&y);
            printf("%lld\n",ask_path_max(x,y));
        }
        if(op[0]=='Q' and op[1]=='S'){
            scanf("%lld %lld",&x,&y);
            printf("%lld\n",ask_path_sum(x,y));
        }
    }
}

标签:剖分,OI,int,树链,ask,节点,now,id,size
From: https://www.cnblogs.com/HaneDaCafe/p/18448012

相关文章

  • NOIP 前 dp 做题小记
    NOIP前dp做题小记[BJOI2019]排兵布阵设\(f(i,j)\)表示在前\(i\)个城堡中总共派遣\(j\)个士兵时,可以获得的最大分数。初始化:\(\forall0\lej\lem\),\(f(0,j)=0\)答案统计:\(ans=f(n,m)\)转移:\(f(i,j)=\max_{0\lek\lej}f(i-1,j-k)+g(i,k)......
  • U486684 「INOI2016」Brackets 题解
    首先对于回文&括号有一个经典转移:枚举分割点+区间两个端点讨论此题也是如此首先枚举分割点十分套路,如下\[dp_{i,j}=\max_{k=i}^{j-1}dp_{i,k}+dp_{k+1,j}\]如果两个端点相同\[dp_{i,j}=dp_{i+1,j-1}+v_i+v_j\]还有一个转移对于一个区间,因为是子序列,所以一个区间的子区间......
  • OI for people in 3000 B.C.
    大家好,我是M先生受B先生之委托,为了给大家最真实的原始人体验,现特将HZOI版本回溯至3000B.C.,在这个版本中,我们做了如下改动:鉴于大家能够说话,我们特地友善地给予了大家充足的讨论时间,而删去3000B.C.时大家还没有进化出来的远程交流功能鉴于远古时期纸币还没有流通,以及......
  • Android 11 如何不要验证Wi-Fi CA 凭证(手工连接WIFI, 需要ROOT)
    Android11如何不要验证Wi-FiCA凭证(手工连接WIFI,需要ROOT)在获取了ROOT权限的基础上,如果因为您机器所使用OS版本的限制无法在GUI界面选择符合您企业设置的WI-FI选项,可以使用本文教程中指出的手工连接WIFI的方式.Step1.检查adbshellsucat/data/misc/apexdata/c......
  • [赛记] 多校A层冲刺NOIP2024模拟赛01【衡中】
    构造字符串50pts错解50pts;考虑正解,对于题目中的要求,我们可以转换成若干个相等与不等的操作,若相等则用并查集合并一下,不等则连边,若同块连边则无解,否则从前往后遍历赋值,每次找所连边其它块值的$\operatorname{mex}$即可;时间复杂度:$\Theta(nm\alpha(n))$;点击查看代码#i......
  • 南沙C++信奥赛陈老师解一本通题: 1828:【02NOIP提高组】均分纸牌
    ​ 【题目描述】有n堆纸牌,编号分别为 1,2,…,n。每堆上有若干张,但纸牌总数必为nn的倍数。可以在任一堆上取若干张纸牌,然后移动。移牌规则为:在编号为1的堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 n 的堆上取的纸牌,只能移到编号为n−1的堆上;其他堆上取的纸牌,可以移到相......
  • 谷歌:过渡到 Rust 使得 Android 漏洞大幅下降
    谷歌:过渡到Rust使得Android漏洞大幅下降来源:OSCHINA编辑: 白开水不加糖2024-09-2810:54:58 6谷歌在最新的一篇文章中指出,内存安全问题导致的漏洞百分比与新代码使用的开发语言密切相关。而随着其将开发转向内存安全语言,Android内存安全漏洞的百分比已经......
  • [Ynoi2012] NOIP2015 充满了希望
    [Ynoi2012]NOIP2015充满了希望题意给一个长为\(n\)的序列,有\(m\)个操作,操作编号从\(1\)到\(m\),每个操作为:1xy:将序列位置为\(x,y\)的两个元素交换。2lrx:将序列区间\([l,r]\)内所有元素修改为\(x\)。3x:查询序列\(x\)位置的值。现在有\(q\)次查询,每次......
  • 题解:P8973 『GROI-R1』 继续深潜,为了同一个梦想
    换根dp模板题。\(f_i\)是在以\(i\)为根的子树中,以\(i\)为链的一个端点且\(i\)在点集中的合法点集个数。\(ans_i\)表示包含\(i\)的合法点集个数。当\(x\)为树根时:\[ans_x={f_x\choose2}-\sum_{s\inson}{2f_s+1\choose2}+f_x\]简单解释一下,\({f_x\ch......
  • P4170 [CQOI2007] 涂色
    算法看完题目不好想到思路逆向思维,考虑从目标串刷成一个由全部相等的颜色组成的串由于一刷刷一堆想到区间状态设\(dp_{l,r}\)表示区间\([l,r]\)的最少涂抹次数状态转移分类讨论\(S_l=S_r\text{且}l<r\)此时分别去掉两个端点,观察发现设覆盖了\(l\)......