树链剖分总结

什么是树链剖分?

  对于一(heng)些(duo)在树上对路径维护的题,就算暴力倍增找lca也会TLE。为此,我们以某种规则将一棵树剖分成若干条竖直方向上的链,每次维护时可以一次跳一条链、并借助一些强大的线性数据结构来维护(通常链的数量很少),这样就大大优化了时间复杂度,足以解决很多线性结构搬到树上的题目。

前置知识:

  以线段树为代表的各种强大的数据结构;dfs序(化树形为线性)

学习前先了解一些新概念:

  重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;

  轻儿子:父亲节点中除了重儿子以外的儿子;

  重边:父亲结点和重儿子连成的边;

  轻边:父亲节点和轻儿子连成的边;

  重链:由多条重边连接而成的路径;

  轻链:由多条轻边连接而成的路径;

通常我们剖分的规则是重链剖分法,即先把重儿子分到一个链上,再去管其余轻儿子。这样做在大多数情况下得到的长链较多,而且有经证明的时间复杂度上限。

这样肯定要先来一遍dfs找到所有重儿子,并预处理一些点的基本信息:深度、父亲节点、字树大小;之后再来一遍dfs,剖分树链(记录每个点的链头),并记录此时的dfs序,将树的节点需维护的信息按dfs序存到一个线性结构中维护。两遍dfs后就在线性结构的上面建一棵线段树(一般都是线段树(毕竟太强大了、太容易被考了),有时换成树状数组也行,看题目而定啦)。

见代码:

 1 void dfs1(int u,int fa,int deep)//第一遍dfs 
 2 {
 3     dep[u]=deep;//深度 
 4     siz[u]=1;//子树大小一开始为1(它自己) ,之后再加它儿子的子树大小就得到的它真正的子树大小了 
 5     int maxs=0,maxd=0;
 6     int t;
 7     for(int e=lst[u];e;e=nxt[e])
 8     {
 9         t=to[e];
10         if(t!=fa)
11         {
12             father[t]=u;
13             dfs1(t,u,deep+1);
14             siz[u]+=siz[t];
15             if(siz[t]>maxs)//找重儿子 
16             {
17                 maxs=siz[t];
18                 maxd=t;
19             }
20         }
21     }
22     son[u]=maxd;//重儿子 
23 }
24 
25 int dfsxu;
26 
27 void dfs2(int u,int fa,int ding)//第二遍dfs 
28 {
29     top[u]=ding;//链头 
30     dfs[u]=++dfsxu;//记录dfs序 
31     xu[dfsxu]=num[u];//按dfs序记录的点要维护的信息 
32     if(!son[u])
33         return;
34     dfs2(son[u],u,ding);//先把重儿子纳入父亲的链中 
35     int t;
36     for(int e=lst[u];e;e=nxt[e])
37     {
38         t=to[e];
39         if(t!=fa&&t!=son[u])//其他轻儿子自辟一链 
40             dfs2(t,u,t);
41     }
42 }
dfs核心代码

我们看看dfs序,发现同一条树链上的节点dfs序都是连续的,同时以一个点为根的子树内所有点的dfs序也是连续的,这样导致什么结果?你若想维护一棵子树或一条树链上的信息,只需知道最小和最大的dfs序,便可用数据结构维护这个区间,缩小维护的时间复杂度(比暴力什么的好太多啦)。

之后维护时怎么办?这里单讲维护x到y路径的情况(维护以u为根的子树的话只要维护区间[dfs[u],dfs[u]+siz[u]-1]就好了):

  分两种情况:

    1、x、y在同一链上(链头相等),只要维护浅(深度小)的点的dfs序到另一个点的dfs序之间的区间就结束了

    2、x、y不在同一条链上,那么就让x或y往上跳,跳到同一个链上为止。跳那个呢?发现两点最终肯定不会同在x和y所在链的链头深度深的那个链,但有可能同在x和y所在链的链头深度浅的那个链,设这时dep[top[x]]>=dep[top[y]](如果假设不成立就交换x、y),把x跳到它的链头的父亲上,期间它会越过若干点,这若干点组成的dfs序的区间(包括x但不包括x跳完后所在的点)即为[dfs[top[x],dfs[x]],维护一下这个区间就好。以此规则不断跳,终能转化到第一种情况。

查询与维护跳点的思路一致,将维护区间改为查询区间就好了。

核心代码:

 1 int x,y,z;
 2 x=read(),y=read(),z=read();//将x到y路径所有点的点权加z 
 3 while(top[x]!=top[y])
 4 {
 5     if(dep[top[x]]<dep[top[y]])  swap(x,y);
 6     modify(1,1,n,dfs[top[x]],dfs[x],z);
 7     x=father[top[x]];
 8 }
 9 if(dep[x]>dep[y]) swap(x,y);
10 modify(1,1,n,dfs[x],dfs[y],z);//线段树区间加 
维护函数内部代码
 1 int x,y,z=0;//查询x到y路径所有点的点权之和 
 2 x=read(),y=read();
 3 while(top[x]!=top[y])
 4 {
 5     if(dep[top[x]]<dep[top[y]])  swap(x,y);
 6     z+=fin(1,1,n,dfs[top[x]],dfs[x]);
 7     x=father[top[x]];
 8 }
 9 if(dep[x]>dep[y]) swap(x,y);
10 z+=fin(1,1,n,dfs[x],dfs[y]);
11 printf("%d
",z%mod);
查询函数内部代码

完整代码:

  1 #include<iostream>
  2 #include<cstdio>
  3 #include<algorithm>
  4 
  5 using namespace std;
  6 
  7 const int N=100000,M=100000;
  8 
  9 int x,n,m,root,mod,num[N+5],tree[N<<2+5],dep[N+5],son[N+5],dfs[N+5],xu[N+5],top[N+5],siz[N+5];
 10 
 11 int father[N+5],lst[N+5],to[N*2],nxt[N*2],cnt,lzy[N<<2];
 12 
 13 char ch;
 14 
 15 inline int read()
 16 {
 17     x=0;
 18     ch=getchar();
 19     while(!isdigit(ch)) ch=getchar();
 20     while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
 21     return x;
 22 }
 23 
 24 inline void addedge(int u,int v)
 25 {
 26     nxt[++cnt]=lst[u];
 27     lst[u]=cnt;
 28     to[cnt]=v;
 29 }
 30 
 31 void dfs1(int u,int fa,int deep)
 32 {
 33     dep[u]=deep;
 34     siz[u]=1;
 35     int maxs=0,maxd=0;
 36     int t;
 37     for(int e=lst[u];e;e=nxt[e])
 38     {
 39         t=to[e];
 40         if(t!=fa)
 41         {
 42             father[t]=u;
 43             dfs1(t,u,deep+1);
 44             siz[u]+=siz[t];
 45             if(siz[t]>maxs)
 46             {
 47                 maxs=siz[t];
 48                 maxd=t;
 49             }
 50         }
 51     }
 52     son[u]=maxd;
 53 }
 54 
 55 int dfsxu;
 56 
 57 void dfs2(int u,int fa,int ding)
 58 {
 59     top[u]=ding;
 60     dfs[u]=++dfsxu;
 61     xu[dfsxu]=num[u];
 62     if(!son[u])
 63         return;
 64     dfs2(son[u],u,ding);
 65     int t;
 66     for(int e=lst[u];e;e=nxt[e])
 67     {
 68         t=to[e];
 69         if(t!=fa&&t!=son[u])
 70             dfs2(t,u,t);
 71     }
 72 }
 73 
 74 inline void updata(int t)
 75 {
 76     tree[t]=(tree[t<<1]+tree[t<<1|1])%mod;
 77 }
 78 
 79 void build(int t,int l,int r)
 80 {
 81     if(l==r)
 82     {
 83         tree[t]=xu[l]%mod;return;
 84     }
 85     build(t<<1,l,(l+r)>>1);
 86     build(t<<1|1,((l+r)>>1)+1,r);
 87     updata(t);
 88 }
 89 
 90 inline void init()
 91 {
 92     for(int i=1;i<=n;i++) num[i]=read();
 93     int u,v;
 94     for(int i=1;i<n;++i)
 95     {
 96         u=read(),v=read();
 97         addedge(u,v);
 98         addedge(v,u);
 99     }
100     dfs1(root,-1,1);
101     dfs2(root,-1,root);
102     build(1,1,n);
103 }
104 
105 inline void pushdown(int t,int l,int r)
106 {
107     if(!lzy[t]) return;
108     lzy[t<<1]=(lzy[t<<1]+lzy[t])%mod;
109     lzy[t<<1|1]=(lzy[t<<1|1]+lzy[t])%mod; 
110     int mid=(l+r)>>1;
111     tree[t<<1]=(tree[t<<1]+(mid-l+1)*lzy[t])%mod;
112     tree[t<<1|1]=(tree[t<<1|1]+(r-mid)*lzy[t])%mod;
113     lzy[t]=0;
114 }
115 
116 void modify(int t,int l,int r,int x,int y,int a)
117 {
118     if(x<=l&&r<=y)
119     {
120         lzy[t]=(lzy[t]+a)%mod;
121         tree[t]=(tree[t]+(r-l+1)*a)%mod;
122         return;
123     }
124     pushdown(t,l,r);
125     int mid=(l+r)>>1;
126     if(x<=mid) modify(t<<1,l,mid,x,y,a);
127     if(y>mid) modify(t<<1|1,mid+1,r,x,y,a);
128     updata(t);
129 }
130 
131 int fin(int t,int l,int r,int x,int y)
132 {
133     if(x<=l&&r<=y)
134         return tree[t];
135     pushdown(t,l,r);
136     int mid=(l+r)>>1,ans=0;
137     if(x<=mid) ans+=fin(t<<1,l,mid,x,y);
138     if(y>mid) ans+=fin(t<<1|1,mid+1,r,x,y);
139     return ans%mod;
140 }
141 
142 inline void gan1()
143 {
144     int x,y,z;
145     x=read(),y=read(),z=read();
146     while(top[x]!=top[y])
147     {
148         if(dep[top[x]]<dep[top[y]])  swap(x,y);
149         modify(1,1,n,dfs[top[x]],dfs[x],z);
150         x=father[top[x]];
151     }
152     if(dep[x]>dep[y]) swap(x,y);
153     modify(1,1,n,dfs[x],dfs[y],z);
154 }
155 
156 inline void gan2()
157 {
158     int x,y,z=0;
159     x=read(),y=read();
160     while(top[x]!=top[y])
161     {
162         if(dep[top[x]]<dep[top[y]])  swap(x,y);
163         z+=fin(1,1,n,dfs[top[x]],dfs[x]);
164         x=father[top[x]];
165     }
166     if(dep[x]>dep[y]) swap(x,y);
167     z+=fin(1,1,n,dfs[x],dfs[y]);
168     printf("%d
",z%mod);
169 }
170 
171 inline void solve()
172 {
173     int type,x,y;
174     for(int i=1;i<=m;++i)
175     {
176         type=read();
177         switch(type)
178         {
179             case 1:gan1();break;
180             case 2:gan2();break;
181             case 3:
182                 x=read(),y=read();
183                 modify(1,1,n,dfs[x],dfs[x]+siz[x]-1,y);
184                 break;
185             case 4:
186                 x=read();
187                 printf("%d
",fin(1,1,n,dfs[x],dfs[x]+siz[x]-1));
188                 break;
189         }
190     }
191 }
192  
193 int main()
194 {
195     n=read(),m=read(),root=read(),mod=read();
196     init();
197     solve();
198     return 0;
199 }
洛谷树链剖分代码

复杂度证明:

  设u通过一条轻边连接它的儿子v,易知size[u]/2 >=size[v],即每走一次轻边,当前子树的大小都至少减了一半,故根节点到任意一个叶子节点最多走log n条轻边,即最多经过log n条链。故单次路径查询时间复杂度为O(log n)。若单次修改的复杂度为log n(这里以线段树维护为例),则总时间复杂度为O(n log ^2 n) 

模板题:洛谷P3384 【模板】树链剖分

简单说下树链剖分的另一种做法:长链剖分,即进行分链时不再优先size最大的儿子,而是deep(深度)最大的儿子。

  有性质:

    1、任意点的任意祖先所在长链长度一定大于等于这个点所在长链长度

    2、所有长链长度之和就是总节点数

    3、一个点到根的路径上经过的短边最多有O(sqrt(n))条。

    证明下第三个性质:不放将重轻的说法改为类似的长短边。每向上走一个短边,当前子树的size至少加已走过的最长链的长度再加1,则当前子树大小>=1+2+。。。+k(k为走的短边数)=k*(k+1)/2,子树大小最大为n,则k最大为O(sqrt(n))。

    可见单次路径查询时间复杂度为O(sqrt(n)),其他情况与重链剖分类似。

参考资料:

树链剖分讲解及总结(重链剖分+长链剖分)

原文地址:https://www.cnblogs.com/InductiveSorting-QYF/p/11543839.html