树链剖分
一、树链剖分的概念和写法
1.1概念
定义:树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
树链剖分(树剖/链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。
树链剖分一般可以做:路径修改\查询,子树修改\查询
一些定义:
定义 重儿子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
定义 轻儿子节点 表示剩余的所有子结点。
从这个结点到重子节点的边为 重边。
到其他轻子节点的边为 轻边。
若干条首尾衔接的重边构成 重链。
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
我们观察图中,重链是以轻儿子或整棵树的根为起点,依次向下连接所有重儿子组成的链。而轻链是以重链为主干链,其分支的链为轻链的,且向外扩展长度为1。
轻链的本质就是一些长度为1的边,将他们作为桥梁连接下一个重链。同时还可以看出,轻儿子(除去叶子结点)都是下一条重链的起始结点。
两遍dfs就是树链剖分的主要处理,通过dfs我们已经保证一条重链上各个节点dfs序连续,那么可以想到,我们可以通过数据结构(以线段树为例)来维护一条重链的信息
第一遍\(dfs\)处理每个点的重儿子\(hs\),节点大小\(sz\),深度\(dep\),每个点的父亲\(f\)
第二遍\(dfs\),求出每个点的\(dfs\)序,重链上的链头的元素\(top\)。
❗关于第二遍\(dfs\):
首先根据\(hs\),先\(dfs\)所有的重链,遍历重链过程中,传递下去\(top\)值;再以重链为主干,遍历所有的轻链,因为轻结点会是重链的起点,因此,又可以得到新的重链,依次下去。同时先\(dfs\)重链,保证了重链的\(dfs\)序的连续的,即不仅在子树里面dfs序是连续的,在重链里面也是连续的。
为什么要记录链头元素?(该部分引用自)
我们得到了一系列的重链,如何对它们快速操作?那么就是记录链头元素。这样我们就可以直接跳到链头元素。但是我们遇到了一个问题:在同一条重链上可以直接跳到头部,如果不是在同一条链上呢?方法是:从这条重链的头部再往上跳,到他的父亲结点,必定在另外一条重链上,然后根据需求继续跳。如图所示,b结点跳跃的过程:
b在{6 b}这条重链,跳至6号结点,从6号再跳到{1 2 4 5 a}这条重链,再跳就是1根节点。到这里,再想想什么是轻链,理解会更深刻“将他们作为“桥梁”连接下一个重链”。
1.2 (重要)性质
经过轻边,子树大小翻倍。一个点往上走,最多走\(O(\log n)\)条轻边。任意两点u,v,最多经过\(O(\log n)\)条。重链也是不超过\(O(\log n)\)段的。
1.3基本实现
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
vector<int>e[N];
int l[N],r[N],id[N],sz[N],hs[N],tot,dep[N],f[N],top[N];
//第一遍dfs处理每个点的重儿子,节点大小,深度,每个点的父亲
void dfs1(int u,int fa)
{
sz[u] = 1;
hs[u] = -1;
dep[u] = dep[fa]+1;
f[u] = fa
for(auto v:e[u])
{
if(v==fa)continue;
dfs1(v,u);
sz[u]+=sz[v];
if(hs[u]==-1||sz[v]>sz[hs[u]])hs[u] = v;
}
}
//第二步dfs,求出每个点的dfs序,重链上的链头的元素
void dfs2(int u,int t)//keep表示需不需要保留当前信息
{
top[u] = t;
l[u] = ++tot;
id[tot] = u;
if(hs[u]!=-1)
dfs2(hs[u],t);//重儿子的集合
for(auto v:e[u])
{
if(v!=fa[u]&&v!=hs[u])//v是轻儿子,它的链头就是它本身
{
dfs2(v,v);
}
}
r[u] = tot;
}
int main()
{
cin>>n;
for(int i = 1;i<=n;i++)
{
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1,0);
dfs2(1,1);
return 0;
}
1.4 简单应用:树链剖分求\(LCA\)
预处理时间复杂度\(O(n)\),查询时间复杂度\(O(\log n)\)
以上图为例,求\(a,b\)的\(LCA\)。
首先思考两个问题:
-
谁先跳?
深度大的先跳。这时候你会想,那\(a,b\)的深度一样呀。不是的,不是直接比较\(a,b\)的深度,而是比较\(a,b\)向上跳一个链的深度。\(b\)结点所在的重链往上跳一个链就是到\(4\)号结点,而\(a\)所在重链,往上跳也就是到\(1\)了。我们需要比较的是\(1\)号和\(4\)号结点的深度,显然\(4\)号深度更大,那么\(b\)先跳。
-
什么时候能判断出\(LCA\)?
如果两个点在同一重链上,那么深度较小的,为\(LCA\),否则还是要不断的跳。\(a、b\)结点,当\(b\)结点跳到\(4\)结点时,\(a\)和\(4\)在同一重链中,即\(top[a]==top[4]\),所以\(4\)为\(a、b\)最终的\(LCA\)。
例题:树上LCA2
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
vector<int>e[N];
int l[N],r[N],id[N],sz[N],hs[N],tot,dep[N],f[N],top[N];
int n,m;
//第一遍dfs处理每个点的重儿子,节点大小,深度,每个点的父亲
void dfs1(int u,int fa)
{
sz[u] = 1;
hs[u] = -1;
dep[u] = dep[fa]+1;
f[u] = fa;
for(auto v:e[u])
{
if(v==fa)continue;
dfs1(v,u);
sz[u]+=sz[v];
if(hs[u]==-1||sz[v]>sz[hs[u]])hs[u] = v;
}
}
//第二步dfs,求出每个点的dfs序,重链上的链头的元素
void dfs2(int u,int t)//keep表示需不需要保留当前信息
{
l[u] = ++tot;
id[tot] = u;
top[u] = t;
if(hs[u]!=-1)
dfs2(hs[u],t);//重儿子的集合
for(auto v:e[u])
{
if(v!=f[u]&&v!=hs[u])//v是轻儿子,它的链头就是它本身
{
dfs2(v,v);
}
}
r[u] = tot;
}
int LCA(int u,int v)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])v = f[top[v]];
else u = f[top[u]];
}
if(dep[u]<dep[v])return u;
else return v;
}
int main()
{
cin>>n;
for(int i = 1;i<n;i++)
{
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1,0);
dfs2(1,1);
cin>>m;
for(int i = 1;i<=m;i++)
{
int u,v;
cin>>u>>v;
cout<<LCA(u,v)<<"\n";
}
return 0;
}
二、树链剖分的路径查询
根据上面第一点中所说的,在第二遍\(dfs\)中,先\(dfs\)重链,再\(dfs\)轻链,这样保证了\(dfs\)序不仅在子树中是连续的,在重链中也是连续的。
此时我们考虑对整个\(dfs\)序建线段树。我们重链的一段,就是\(dfs\)序的一段。
考虑一个点\(v\)跳到\(f[top[v]]\),我们需要维护的就是\(top[v]\)到\(v\)的信息,再让\(v = f[top[v]]\)。即每跳一段就维护一段的信息。即在\(dfs\)序中\(l[top[v]]\)到\(l[v]\)这一段(\(l[]\)就是\(dfs\)序的编号)
例题:SDOI2011, 染色
题意:
给定一棵\(n\)个节点的无根树,共有$ m$ 个操作,操作分为两种:
- 将节点 \(a\) 到节点 \(b\) 的路径上的所有点(包括 \(a\)和$ b$)都染成颜色 \(c\)。
- 询问节点 \(a\)到节点 \(b\)的路径上的颜色段数量。
颜色段的定义是极长的连续相同颜色被认为是一段。例如 112221
由三段组成:11
、222
、1
。
思路:考虑从\(a\)到\(b\)的路径,那要考虑要两边一起跳。
注意点如下图:
\(dfs\)序和我们实际路径可能是反着的,这个要注意!
注意拼接顺序❗\(ansu = ansu + query\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
vector<int>e[N];
int l[N],r[N],idx[N],sz[N],hs[N],tot,dep[N],f[N],top[N],w[N];
int n,m;
struct info
{
int lc,rc,seg;
};
info operator+(const info &l,const info &r)
{
return (info){l.lc,r.rc,l.seg+r.seg+(l.rc!=r.lc)};
}
struct node{
info val;
int t;
}seg[N*4];
void settag(int id,int t)
{
seg[id].val = {t,t,0};
seg[id].t = t;
}
void pushdown(int id)
{
if(seg[id].t!=0)
{
settag(id*2,seg[id].t);
settag(id*2+1,seg[id].t);
seg[id].t = 0;
}
}
void update(int id)
{
seg[id].val = seg[id*2].val+seg[id*2+1].val;
}
void build(int id,int l,int r)
{
if(l==r)
{
//l号点,dfs序中第l个点,而不是a[l],这里要注意❗
//seg[id].val = {a[l],a[l]};
seg[id].val = {w[idx[l]],w[idx[l]],0};
}
else
{
int mid = (l+r)>>1;
build(id*2,l,mid);
build(id*2+1,mid+1,r);
update(id);
}
}
void modify(int id,int l,int r,int x,int y,int t){
if(l==x&&r==y)
{
settag(id,t);
return;
}
int mid = (l+r)/2;
pushdown(id);
if(y<=mid) modify(id*2,l,mid,x,y,t);
else if(x>mid) modify(id*2+1,mid+1,r,x,y,t);
else{
modify(id*2,l,mid,x,mid,t),modify(id*2+1,mid+1,r,mid+1,y,t);
}
update(id);
}
//O(logn)
info query(int id,int l,int r,int x,int y)
{
if(l==x&&r==y)return seg[id].val;
int mid = (l+r)/2;
pushdown(id);
if(y<=mid)return query(id*2,l,mid,x,y);
else if(x>mid)return query(id*2+1,mid+1,r,x,y);
else{
return query(id*2,l,mid,x,mid)+query(id*2+1,mid+1,r,mid+1,y);
}
}
//第一遍dfs处理每个点的重儿子,节点大小,深度,每个点的父亲
void dfs1(int u,int fa)
{
sz[u] = 1;
hs[u] = -1;
dep[u] = dep[fa]+1;
f[u] = fa;
for(auto v:e[u])
{
if(v==fa)continue;
dfs1(v,u);
sz[u]+=sz[v];
if(hs[u]==-1||sz[v]>sz[hs[u]])hs[u] = v;
}
}
//第二步dfs,求出每个点的dfs序,重链上的链头的元素
void dfs2(int u,int t)//keep表示需不需要保留当前信息
{
l[u] = ++tot;
idx[tot] = u;
top[u] = t;
if(hs[u]!=-1)
dfs2(hs[u],t);//重儿子的集合
for(auto v:e[u])
{
if(v!=f[u]&&v!=hs[u])//v是轻儿子,它的链头就是它本身
{
dfs2(v,v);
}
}
r[u] = tot;
}
int LCA(int u,int v)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])v = f[top[v]];
else u = f[top[u]];
}
if(dep[u]<dep[v])return u;
else return v;
}
int query(int u,int v)
{
info ansu = {0,0,-1},ansv={0,0,-1};
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]){
ansv = query(1,1,n,l[top[v]],l[v]) + ansv;//在前面加一段
v = f[top[v]];
}
else{
ansu = query(1,1,n,l[top[u]],l[u]) + ansu;
u = f[top[u]];
}
}
if(dep[u]<=dep[v])ansv = query(1,1,n,l[u],l[v])+ansv;
else ansu = query(1,1,n,l[v],l[u])+ansu;
return ansu.seg+ansv.seg+(ansu.lc!=ansv.lc)+1;
}
void modify(int u,int v,int c)
{
// while(top[u]!=top[v])
// {
// if(dep[top[u]]<dep[top[v]]){
// modify(1,1,n,l[top[v]],l[v],c);
// v = f[top[v]];
// }
// else{
// modify(1,1,n,l[top[u]],l[u],c);
// u = f[top[u]];
// }
// }
// if(dep[u]<=dep[v])modify(1,1,n,l[u],l[v],c);
// else modify(1,1,n,l[v],l[u],c);
while(top[u]!=top[v])
{
if(dep[top[u]]>dep[top[v]])swap(u,v);
modify(1,1,n,l[top[v]],l[v],c);
v = f[top[v]];
}
if(dep[u]>dep[v])swap(u,v);
modify(1,1,n,l[u],l[v],c);
}
int main()
{
cin>>n>>m;
for(int i = 1;i<=n;i++)
cin>>w[i];
for(int i = 1;i<n;i++)
{
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1,0);
dfs2(1,1);
build(1,1,n);
for(int i = 1;i<=m;i++)
{
char op;
cin>>op;
if(op=='Q')
{
int u,v;
cin>>u>>v;
cout<<query(u,v)<<endl;
}
else
{
int u,v,c;
cin>>u>>v>>c;
modify(u,v,c);
}
}
return 0;
}
三、树链剖分的子树查询
例题:NOI2015, 软件包管理器
题意:要下载某一个软件,就要把该软件到根路径上的所有没下载的软件都下载了。问每次操作完,有多少个软件包状态发生改变(从安装到没安装,或者从没安装到安装)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
vector<int>e[N];
int l[N],r[N],idx[N],sz[N],hs[N],tot,dep[N],f[N],top[N],w[N];
int n,m;
struct node{
int cnt;
int sz;
int t;
}seg[N*4];
void settag(int id,int t)
{
if(t==1)
seg[id].cnt = seg[id].sz;
else seg[id].cnt = 0;
seg[id].t = t;
}
void pushdown(int id)
{
if(seg[id].t!=-1)
{
settag(id*2,seg[id].t);
settag(id*2+1,seg[id].t);
seg[id].t = -1;
}
}
void update(int id)
{
seg[id].cnt= seg[id*2].cnt+seg[id*2+1].cnt;
}
void build(int id,int l,int r)
{
seg[id].sz = (r-l+1);
seg[id].t = -1;
if(l==r)
{
seg[id].cnt = 0;
}
else
{
int mid = (l+r)>>1;
build(id*2,l,mid);
build(id*2+1,mid+1,r);
update(id);
}
}
void modify(int id,int l,int r,int x,int y,int t){
if(l==x&&r==y)
{
settag(id,t);
return;
}
int mid = (l+r)/2;
pushdown(id);
if(y<=mid) modify(id*2,l,mid,x,y,t);
else if(x>mid) modify(id*2+1,mid+1,r,x,y,t);
else{
modify(id*2,l,mid,x,mid,t),modify(id*2+1,mid+1,r,mid+1,y,t);
}
update(id);
}
//第一遍dfs处理每个点的重儿子,节点大小,深度,每个点的父亲
void dfs1(int u,int fa)
{
sz[u] = 1;
hs[u] = -1;
dep[u] = dep[fa]+1;
f[u] = fa;
for(auto v:e[u])
{
if(v==fa)continue;
dfs1(v,u);
sz[u]+=sz[v];
if(hs[u]==-1||sz[v]>sz[hs[u]])hs[u] = v;
}
}
//第二步dfs,求出每个点的dfs序,重链上的链头的元素
void dfs2(int u,int t)//keep表示需不需要保留当前信息
{
l[u] = ++tot;
idx[tot] = u;
top[u] = t;
if(hs[u]!=-1)
dfs2(hs[u],t);//重儿子的集合
for(auto v:e[u])
{
if(v!=f[u]&&v!=hs[u])//v是轻儿子,它的链头就是它本身
{
dfs2(v,v);
}
}
r[u] = tot;
}
void install(int x)
{
while(x!=0)
{
modify(1,1,n,l[top[x]],l[x],1);
x = f[top[x]];
}
}
void uninstall(int x)
{
modify(1,1,n,l[x],r[x],0);
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i = 2;i<=n;i++)
{
cin>>f[i];
++f[i];
e[f[i]].push_back(i);
}
dfs1(1,0);
dfs2(1,1);
build(1,1,n);
cin>>m;
int pre = 0;
for(int i = 1;i<=m;i++)
{
string op;
int x;
cin>>op>>x;
x++;
if(op=="install")
{
install(x);
}
else
{
uninstall(x);
}
cout<<abs(seg[1].cnt-pre)<<"\n";
pre = seg[1].cnt;
}
return 0;
}
四、总结
树链剖分把路径问题转为为\(O(\log n)\)个区间问题。