带你从模板理解树链剖分


树链剖分总结:


这几天也是学习了树链剖分,刚开始一脸懵逼,完全不知道在讲什么东西,现在终于理解了一点,感觉要想理解这个算法的话,其实结合着图题目加模板来看的话是最快的。


首先是概述(纯属个人瞎扯,扯错了请各位大佬指出,感激不尽)


树链剖分其实很像一类题目,就是将一棵树转换为线性结构,通过dfs序来遍历最终使得树上的结点成为线性的连续区间,然后利用一系列数据结构进行解题(弱鸡如我当前只会线段树),树链剖分其实就是将树上的边转换为了线性结构,但是转换的时候通过一些办法保证了时间复杂度。反正我他喵是不会证,


谈一下理解吧,个人觉得先讲明一些概念比较好,只讲比较关键的几个,首先是重儿子,u的重儿子其实就是以u为父结点的所有子结点中子树最多的那一个儿子节点,可以简单的理解为最重最复杂的那个儿子,重链呢就是把所有重儿子连接起来的链就是重链,废话不多说,上图,图是网上找的,侵删。

                                                                                                                                         


暂时先别管重链怎么得到的,可以看到上面的1 4 9 13 14 是明显的重链,从头连到尾,那么其他黑色边代表了其他的重链呢,其实就是对于每一个结点来说,都延伸了一个向下的重链,这样我们可以看到链上的编号是线性连续的,那么我们就可以利用数据结构来维护了。


接下来是比较关键的查询操作,查询某u,v结点间的所有边。

首先令 f1=top[u],f2=top[v];

首先判断u,v是否在一条重链上,(在此先默认是u的深度小于v,即v在u的下面),那么直接查询即可。若不在一条重链上的话,假设f1深度大于f2,那么先查询w[f1],w[u],然后令u=fa[f1],即向上爬一格。

拿上面的图举个例子,当我查询11,14的时候,我会先判断发现不在一条重链上,然后发现top[11]比top[14]大,接着查询 9 10 11号点,然后u爬到了1,接着发现1和14在同一条重链上,所以直接查询即可。



理解上面的思想后,接下来主要是通过模板和题目进行一些细节部分的讲解,模板包括两个dfs,我的模板用的是链式前向星写法,理解了这个vector写法应该不难,


以下是模板中各个数组的作用,懒得自己写了,用一下大佬的。

size[]数组,用来保存以当前节点为根的子树节点个数

top[]数组,用来保存当前节点的所在链的顶端节点

son[]数组,用来保存重儿子

dep[]数组,用来保存当前节点的深度

fa[]数组,用来保存当前节点的父亲

w[]数组,用来保存树中每个节点剖分后的新编号


以下贴代码,为spoj的Qtree,就是单纯的查询操作,也算是模板题了,通过这道题讲一下树链剖分应用部分的细节,


#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <cmath>
#include <vector>
#include <queue>
#include <map>
#include <algorithm>
#include <set>
#include <functional>
#define pb push_back
#define lson rt<<1
#define rson rt<<1|1
using namespace std;
typedef long long ll;
const int N=10010;
const int INF=1e9+7;
int cnt,n,step,ans;
int dep[N],size[N],son[N],top[N],w[N],fa[N];
int head[N];
char s[20];
struct node //链式前向星
{
    int u,v,w;
    int next;
}edge[N<<1];
struct tree
{
    int l,r,mid;
    int ma;
}t[N<<2];
void add(int u,int v,int w) //建边注意cnt从0开始
{
    edge[cnt].u=u;edge[cnt].v=v;edge[cnt].w=w;
    edge[cnt].next=head[u];head[u]=cnt++;
    return ;
}
void dfs1(int u,int fat,int de)     //dfs1找出每个结点的重儿子,父亲结点,深度
{
    dep[u]=de;fa[u]=fat;size[u]=1;son[u]=0;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;    //当前结点所有的子节点
        if(v==fat)      //这里是因为无向边的问题
            continue;
        dfs1(v,u,de+1);
        size[u]+=size[v];
        if(son[v]==-1||(size[son[u]]<size[v]))  //找出重儿子
            son[u]=v;
    }
    return ;
}
void dfs2(int u,int tp) //dfs2将重链连接起来
{
    top[u]=tp;      //更新当前结点所在链的头结点
    w[u]=++step;    //更新u结点编号
    if(son[u]>0)    //优先连接重儿子形成重链
        dfs2(son[u],tp);
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;    //从其他儿子开始往下重新连接重链
        if(v==fa[u]||v==son[u]) //排除一些不可能的选项
            continue;
        dfs2(v,v);
    }
    return ;
}
void build(int l,int r,int rt)  //线段树部分就不写了
{
    int m=(l+r)>>1;
    t[rt].l=l;t[rt].r=r;
    t[rt].mid=m;
    t[rt].ma=0;
    if(l==r)
        return ;
    build(l,m,lson);
    build(m+1,r,rson);
}
void pushup(int rt)
{
    t[rt].ma=max(t[lson].ma,t[rson].ma);
}
void query(int p,int q,int rt)
{
    if(t[rt].l>=p&&t[rt].r<=q)
    {
        ans=max(ans,t[rt].ma);
        return ;
    }
    if(p<=t[rt].mid)
        query(p,q,lson);
    if(q>t[rt].mid)
        query(p,q,rson);
    pushup(rt);
}
void update(int pos,int flag,int rt)
{
    if(t[rt].l==t[rt].r)
    {
        t[rt].ma=flag;
        return ;
    }
    if(pos<=t[rt].mid)
        update(pos,flag,lson);
    if(pos>t[rt].mid)
        update(pos,flag,rson);
    pushup(rt);
}
void vs(int u,int v)
{
    int f1,f2;
    ans=-INF;
    while(u!=v) //这里就是查询操作了 写法比较多 这只是其中一种
    {
        if(dep[u]>dep[v])   //首先判断深度
            swap(u,v);
        f1=top[u];f2=top[v];    
        if(f1==f2)  //同一条重链上
        {
            query(w[son[u]],w[v],1);
            v=u;
        }
        else if(dep[f1]>dep[f2])    //不在同一条重链上按照之前说的先查询深度大的链
        {
            query(w[f1],w[u],1);
            u=fa[f1];       //结点向上爬一位
        }
        else    //同上
        {
            query(w[f2],w[v],1);
            v=fa[f2];
        }
    }
    if(ans==-INF)
        ans=0;
    printf("%d\n",ans);
    return ;
}
void slove()
{
    build(0,step,1);
    for(int i=0;i<n-1;i++)  //注意这里的范围 因为用的链式前向星建的无向边
    {                           
        int x=edge[i*2].u;  //下面每个下标 *2 保证边的不重复
        int y=edge[i*2].v;
        if(dep[x]>dep[y])   //判断深度
            swap(edge[i*2].u,edge[i*2].v);
        update(w[edge[i*2].v],edge[i*2].w,1);   //更新边的权值
    }
    int p,q;
    while(scanf(" %s",s))
    {
        if(s[0]=='D')
            break;
        scanf("%d%d",&p,&q);
        if(s[0]=='Q')
            vs(p,q);
        if(s[0]=='C')
            update(w[edge[(p-1)*2].v],q,1); //直接更新 注意*2
    }
}
int main()
{
    int QAQ;
    scanf("%d",&QAQ);
    while(QAQ--)
    {
        cnt=0;
        memset(head,-1,sizeof head);    //链式前向星初始化
        scanf("%d",&n);
        int u,v,w;
        for(int i=1;i<n;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            add(u,v,w);     //建边 注意无向
            add(v,u,w);
        }
        step=0;     //编号初始化
        dfs1(1,0,1);
        dfs2(1,1);
        slove();
    }
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值