带你从模板理解树链剖分

来源:互联网 发布:驱动精灵软件 编辑:程序博客网 时间:2024/06/07 01:14


树链剖分总结:


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


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


树链剖分其实很像一类题目,就是将一棵树转换为线性结构,通过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|1using 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();    }}




阅读全文
0 0
原创粉丝点击