树链剖分学习笔记
树链剖分,简称树剖,用来解决一类维护静态树上路径信息的问题。就是对树进行一些操作,使其变成线性的结构,从而用线段树进行维护。
树链剖分的方法是轻重边剖分。我们将树上的边分成两种——轻边和重边。如果我们记siz[u]表示节点u的子树大小,在u的所有儿子之中,节点v的siz最大,则将边(u,v)称为重边,v称为u的重儿子。u到其他儿子的边就是轻边,其他的儿子就是轻儿子。
对于轻重边有这样的性质:
1.轻儿子的子树大小小于或等于其父亲子树大小的一半。即若(u,v)为轻边,满足siz[v] <= siz[u] / 2。
证明其实很显然,假设存在一个轻儿子x使得siz[x] > siz[u] / 2,则u其他儿子siz之和都小于siz[x],那么x必然是重儿子,与假设矛盾,因此原命题成立。
2.从根节点到某个节点v路径上的轻边个数不多于O(logn)。
证明:显然如果是叶子结点可以满足边的数量最多,假设有一个叶子结点v满足路径上的边均为轻边,而由上一条性质可知,每次经过轻边实际上就是子树规模减少了一半,因此至多经过O(logn)条轻边就可以到达v。
3.定义重路径为一条路径所有的边都为重边(特别地,一个点也是一条重路径)。则对于每个节点到根节点的路径上都有不超过O(logn)条轻边和O(logn)条重路径。这个由性质2显然可以得到证明,因为轻边已经不超过O(logn),除了轻边都是重路径,因此也不超过O(logn)。
由以上三条性质可知,假如我们实现了对树进行轻重边剖分,那么每次处理路径复杂度都是O(logn)的,显然复杂度非常优秀。
现在我们就需要考虑怎么实现轻重边剖分了。
定义变量:
链前存图:head[N],nxt[M],to[M],N为节点个数,M为边的数量。
线段树相关:tree[N * 4],lazy[N * 4]
树上信息:
dep[N]维护节点到根节点距离。
fa[N]维护节点父亲。
siz[N]维护节点大小。
son[N]维护节点重儿子编号。
top[N]维护一条重路径上dep值最小的点的编号。
dfn[N],pos[N]维护节点dfs序,dfn[x]为dfs序为x的节点编号,pos[u]为节点u对应的dfs序编号,二者互相对应。
til[N]维护dfs序上子树大小,[ pos[u],til[u] ]即一段存储u子树的序列。
好的相关变量定义完了(为什么这么多qwq)。
首先我们需要进行一些dfs操作维护这些变量:
第一次dfs,维护每个节点的父亲,深度,子树大小,重儿子编号。这个操作十分显然。
void dfs1(int u)
{
dep[u] = dep[fa[u]] + 1;
siz[u] = 1;
for(int i = head[u];i;i = nxt[i])
{
int v = to[i];
if(v == fa[u]) continue;
fa[v] = u;
dfs1(v);
siz[u] += siz[v];
if(siz[v] > siz[son[u]]) son[u] = v;//当一个儿子的siz比当前重儿子大就更新重儿子。
}
return;
}
第二次dfs,维护重路径上的top,dfs序,子树序列。这里一定要优先遍历重儿子,使得重路径在线段树中的位置是连续的。
void dfs2(int u)
{
if(u == son[fa[u]]) top[u] = top[fa[u]];
else top[u] = u;
dfn[++tot] = u;
pos[u] = tot;
if(son[u]) dfs2(son[u]);
for(int i = head[u];i;i = nxt[i])
{
int v = to[i];
if(v == son[u] || v == fa[u]) continue;
dfs2(v);
}
til[u] = tot;
return;
}
这样两次dfs我们就成功维护好了重路径和树上信息了。然后就要进行路径修改操作了,通过递归实现。
如果两个节点就在同一条重路径上,那么线段树中这两个点之间的区间也是连续的,直接修改就行。如果不在同一路径上,那么就需要进行“跳”的操作,也就是将深度较大的点向上跳,直接跳到当前重路径的顶端,然后继续进行这个操作直到两节点位于同一重路径。我们发现最坏情况下就是两个节点都需要跳到根节点处,而由前面论证可知这样跳的次数不会超过O(logn)次,每次进行一次线段树修改,复杂度O(logn),因此进行一次路径修改的总复杂度为O(log²n)。
void path_change(int u,int v,int k)
{
if(top[u] == top[v])
{
if(pos[u] > pos[v]) swap(u,v);
modify(1,1,n,pos[u],pos[v],k);
return;
}
if(dep[top[u]] > dep[top[v]]) swap(u,v);
modify(1,1,n,pos[top[v]],pos[v],k);
path_change(u,fa[top[v]],k);
return;
}
然后是路径查询,其实同理,只需要将路径修改中的修改部分改成查询就行,其实就是查询路径分成多条重路径上的答案然后求和。复杂度显然也是O(log²n)的。
int path_query(int u,int v)
{
if(top[u] == top[v])
{
if(pos[u] > pos[v]) swap(u,v);
return query(1,1,n,pos[u],pos[v]) % mod;
}
if(dep[top[u]] > dep[top[v]]) swap(u,v);
return (query(1,1,n,pos[top[v]],pos[v]) + path_query(u,fa[top[v]])) % mod;
}
至于对子树进行修改,查询,只需要对[pos[x],til[x]]进行常规线段树修改查询操作就行了。
贴一个洛谷P3384树链剖分的完整代码:
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<ctime>
#include<cstdlib>
#include<set>
#include<queue>
#include<vector>
#include<string>
using namespace std;
#define P system("pause");
#define A(x) cout << #x << " " << (x) << endl;
#define AA(x,y) cout << #x << " " << (x) << #y << " " << (y) << endl;
#define ll long long
#define inf 1000000000
#define linf 10000000000000000
#define mem(x) memset(x,0,sizeof(x))
int read()
{
int x = 0,f = 1;
char c = getchar();
while(c < '0' || c > '9')
{
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9')
{
x = (x << 3) + (x << 1) + c - '0';
c = getchar();
}
return f * x;
}
#define ls (x << 1),l,mid
#define rs (x << 1 | 1),mid + 1,r
#define mid ((l + r) >> 1)
#define N 1000010
#define M N << 1
int tree[N << 2],lazy[N << 2],mod;
int n,m,r,cnt;
int head[N],nxt[M],to[M],val[N],fa[N],dep[N],siz[N],son[N],top[N];
int dfn[N],pos[N],til[N],tot;
void push_up(int x)
{
tree[x] = (tree[x << 1] + tree[x << 1 | 1]) % mod;
return;
}
void build(int x,int l,int r)
{
if(l == r)
{
tree[x] = val[dfn[l]] % mod;
return;
}
build(ls);
build(rs);
push_up(x);
return;
}
void Add(int x,int l,int r,int k)
{
tree[x] = (1ll * tree[x] + (r - l + 1) * k) % mod;
lazy[x] = (lazy[x] + k) % mod;
return;
}
void push_down(int x,int l,int r)
{
if(!lazy[x]) return;
Add(ls,lazy[x]);
Add(rs,lazy[x]);
lazy[x] = 0;
return;
}
void modify(int x,int l,int r,int p,int q,int k)
{
if(p <= l && r <= q)
{
Add(x,l,r,k);
return;
}
push_down(x,l,r);
if(p <= mid) modify(ls,p,q,k);
if(q > mid) modify(rs,p,q,k);
push_up(x);
return;
}
int query(int x,int l,int r,int p,int q)
{
if(p <= l && r <= q) return tree[x] % mod;
int ret = 0;
push_down(x,l,r);
if(p <= mid) ret = (ret + query(ls,p,q)) % mod;
if(q > mid) ret = (ret + query(rs,p,q)) % mod;
return ret % mod;
}
void add(int u,int v)
{
nxt[++cnt] = head[u];
head[u] = cnt;
to[cnt] = v;
}
void dfs1(int u)
{
dep[u] = dep[fa[u]] + 1;
siz[u] = 1;
for(int i = head[u];i;i = nxt[i])
{
int v = to[i];
if(v == fa[u]) continue;
fa[v] = u;
dfs1(v);
siz[u] += siz[v];
if(siz[v] > siz[son[u]]) son[u] = v;
}
return;
}
void dfs2(int u)
{
if(u == son[fa[u]]) top[u] = top[fa[u]];
else top[u] = u;
dfn[++tot] = u;
pos[u] = tot;
if(son[u]) dfs2(son[u]);
for(int i = head[u];i;i = nxt[i])
{
int v = to[i];
if(v == son[u] || v == fa[u]) continue;
dfs2(v);
}
til[u] = tot;
return;
}
void path_change(int u,int v,int k)
{
if(top[u] == top[v])
{
if(pos[u] > pos[v]) swap(u,v);
modify(1,1,n,pos[u],pos[v],k);
return;
}
if(dep[top[u]] > dep[top[v]]) swap(u,v);
modify(1,1,n,pos[top[v]],pos[v],k);
path_change(u,fa[top[v]],k);
return;
}
int path_query(int u,int v)
{
if(top[u] == top[v])
{
if(pos[u] > pos[v]) swap(u,v);
return query(1,1,n,pos[u],pos[v]) % mod;
}
if(dep[top[u]] > dep[top[v]]) swap(u,v);
return (query(1,1,n,pos[top[v]],pos[v]) + path_query(u,fa[top[v]])) % mod;
}
int main()
{
n = read(),m = read(),r = read(),mod = read();
for(int i = 1;i <= n;i++) val[i] = read();
for(int i = 1,u,v;i < n;i++)
{
u = read(),v = read();
add(u,v);
add(v,u);
}
dfs1(r);
dfs2(r);
build(1,1,n);
int op,x,y,z;
while(m--)
{
op = read(),x = read();
switch(op)
{
case 1:
y = read(),z = read();
path_change(x,y,z);
break;
case 2:
y = read();
printf("%d\n",path_query(x,y) % mod);
break;
case 3:
z = read();
modify(1,1,n,pos[x],til[x],z);
break;
case 4:
printf("%d\n",query(1,1,n,pos[x],til[x]) % mod);
break;
}
}
return 0;
}
好的树链剖分常用操作就完了,就是将树形结构变成线性结构处理,非常便捷,虽然码量不小,但其实并不难写。