首页 > 其他分享 >树链剖分

树链剖分

时间:2024-08-26 20:26:09浏览次数:2  
标签:剖分 int top DFS 树链 dep 重链 节点

树链剖分的思想及能解决的问题

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

树链剖分(树剖/链剖)有多种形式,如 重链剖分长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。

重链剖分可以将树上的任意一条路径划分成不超过 \(O(\log n)\) 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。

重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。

如:

  1. 修改 树上两点之间的路径上 所有点的值。
  2. 查询 树上两点之间的路径上 节点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)

除了配合数据结构来维护树上路径信息,树剖还可以用来 \(O(\log n)\)(且常数较小)地求 LCA。在某些题目中,还可以利用其性质来灵活地运用树剖。

重链剖分

我们给出一些定义:

定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。

定义 轻子节点 表示剩余的所有子结点。

从这个结点到重子节点的边为 重边

到其他轻子节点的边为 轻边

若干条首尾衔接的重边构成 重链

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

如图:

在这里插入图片描述

实现

树剖的实现分两个 DFS 的过程。代码如下:

第一个 DFS 记录每个结点的父节点(fa)、深度(dep)、子树大小(sz)、重子节点(hs)。

// 第一遍DFS,子树大小,重儿子,父亲,深度
void dfs1(int u,int f)
{
	sz[u]=1;
	hs[u]=-1;
	fa[u]=f;
	dep[u]=dep[f]+1;
	for(auto v : e[u])
	{
		if(v==f) continue;
		dfs1(v,u);
		sz[u]+=sz[v];
		if(hs[u] == -1 || sz[hs[u]] < sz[v]) hs[u]=v;
	}
}

第二个 DFS 记录所在链的链顶(top,应初始化为结点本身)、重边优先遍历时的 DFS 序(id)、DFS 序对应的节点编号()。

// 第二遍DFS,每一个点DFS序,重链上的链头的元素
void dfs2(int u,int t)
{
	l[u]=++tot;
	top[u]=t;
	id[tot]=u;
	if(hs[u]!=-1)
	{
		dfs2(hs[u],t);
	}
	for(auto v : e[u])
	{
		if(v!=hs[u]&&v!=fa[u])
		{
			dfs2(v,v);
		}
	}
	r[u]=tot;
}

以下为代码实现。

我们先给出一些定义:

  • \(fa(x)\) 表示节点 \(x\) 在树上的父亲。
  • \(dep(x)\) 表示节点 \(x\) 在树上的深度。
  • \(siz(x)\) 表示节点 \(x\) 的子树的节点个数。
  • \(hs(x)\) 表示节点 \(x\) 的 重儿子
  • \(top(x)\) 表示节点 \(x\) 所在 重链 的顶部节点(深度最小)。
  • \(dfn(x)\) 表示节点 \(x\) 的 DFS 序,也是其在树中的编号。

我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 \(fa(x)\),\(dep(x)\),\(siz(x)\),\(son(x)\),第二次 DFS 求出 \(top(x)\),\(dfn(x)\),\(rnk(x)\)。

重链剖分的性质

树上每个节点都属于且仅属于一条重链

重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)。

所有的重链将整棵树 完全剖分

在剖分时 重边优先遍历,最后树的 DFS 序上,重链内的 DFS 序是连续的。按 DFN 排序后的序列即为剖分后的链。

一颗子树内的 DFS 序是连续的。

可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。

因此,对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 \(O(\log n)\) 次,因此,树上的每条路径都可以被拆分成不超过 \(O(\log n)\) 条重链。

常见应用

路径上维护

用树链剖分求树上两点路径权值和,伪代码如下:

\[\begin{array}{l} \text{TREE-PATH-SUM }(u,v) \\ \begin{array}{ll} 1 & tot\gets 0 \\ 2 & \textbf{while }u.top\text{ is not }v.top \\ 3 & \qquad \textbf{if }u.top.deep< v.top.deep \\ 4 & \qquad \qquad \text{SWAP}(u, v) \\ 5 & \qquad tot\gets tot + \text{sum of values between }u\text{ and }u.top \\ 6 & \qquad u\gets u.top.father \\ 7 & tot\gets tot + \text{sum of values between }u\text{ and }v \\ 8 & \textbf{return } tot \end{array} \end{array} \]

链上的 DFS 序是连续的,可以使用线段树、树状数组维护。

每次选择深度较大的链往上跳,直到两点在同一条链上。

同样的跳链结构适用于维护、统计路径上的其他信息。

子树维护

有时会要求,维护子树上的信息,譬如将以 \(x\) 为根的子树的所有结点的权值增加 \(v\)。

在 DFS 搜索的时候,子树中的结点的 DFS 序是连续的。

每一个结点记录 bottom 表示所在子树连续区间末端的结点。

这样就把子树信息转化为连续的一段区间信息。

求最近公共祖先

P3379 【模板】最近公共祖先(LCA)

不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。

向上跳重链时需要先跳所在重链顶端深度较大的那个。

int LCA(int u,int v)
{
	while(top[u]!=top[v])
	{
		if(dep[top[u]] < dep[top[v]]) v=fa[top[v]];
		else u=fa[top[u]];
	}
	if(dep[u]<dep[v]) return u;
	else return v;
}

参考代码:

#include <bits/stdc++.h>
#define endl "\n"
#define int long long
using namespace std;

const int N = 5e5 + 3;

using i64 = long long;
int fa[N],hs[N],sz[N],id[N];
int top[N],dep[N],tot,l[N],r[N];
int n,m,s;
vector<int> e[N];
// 第一遍DFS,子树大小,重儿子,父亲,深度
void dfs1(int u,int f)
{
	sz[u]=1;
	hs[u]=-1;
	fa[u]=f;
	dep[u]=dep[f]+1;
	for(auto v : e[u])
	{
		if(v==f) continue;
		dfs1(v,u);
		sz[u]+=sz[v];
		if(hs[u] == -1 || sz[hs[u]] < sz[v]) hs[u]=v;
	}
}
// 第二遍DFS,每一个点DFS序,重链上的链头的元素
void dfs2(int u,int t)
{
	l[u]=++tot;
	top[u]=t;
	id[tot]=u;
	if(hs[u]!=-1)
	{
		dfs2(hs[u],t);
	}
	for(auto v : e[u])
	{
		if(v!=hs[u]&&v!=fa[u])
		{
			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=fa[top[v]];
		else u=fa[top[u]];
	}
	if(dep[u]<dep[v]) return u;
	else return v;
}
signed main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m>>s;
	for(int i=1;i<n;i++)
	{
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs1(s,-1);
	dfs2(s,s);
	while(m--)
	{
		int a,b;
		cin>>a>>b;
		cout<<LCA(a,b)<<endl;
	}
}



例题

「ZJOI2008」树的统计

题目大意

对一棵有 \(n\) 个节点,节点带权值的静态树,进行三种操作共 \(q\) 次:

  1. 修改单个节点的权值;
  2. 查询 \(u\) 到 \(v\) 的路径上的最大权值;
  3. 查询 \(u\) 到 \(v\) 的路径上的权值之和。

保证 \(1\le n\le 30000\),\(0\le q\le 200000\)。

解法

根据题面以及以上的性质,你的线段树需要维护三种操作:

  1. 单点修改;
  2. 区间查询最大值;
  3. 区间查询和。

单点修改很容易实现。

由于子树的 DFS 序连续(无论是否树剖都是如此),修改一个节点的子树只用修改这一段连续的 DFS 序区间。

问题是如何修改/查询两个节点之间的路径。

考虑我们是如何用 倍增法求解 LCA 的。首先我们 将两个节点提到同一高度,然后将两个节点一起向上跳。对于树链剖分也可以使用这样的思想。

在向上跳的过程中,如果当前节点在重链上,向上跳到重链顶端,如果当前节点不在重链上,向上跳一个节点。如此直到两节点相同。沿途更新/查询区间信息。

对于每个询问,最多经过 \(O(\log n)\) 条重链,每条重链上线段树的复杂度为 \(O(\log n)\),因此总时间复杂度为 \(O(n\log n+q\log^2 n)\)。实际上重链个数很难达到 \(O(\log n)\)(可以用完全二叉树卡满),所以树剖在一般情况下常数较小。

给出一种代码实现:

#include<bits/stdc++.h>
#define endl "\n"
#define int long long
using namespace std;
const int N=2e5+3;
using i64 = long long;
int n,q;
int l[N],r[N],id[N],dep[N],hs[N],sz[N];
int fa[N],top[N],a[N],tot;
vector<int> e[N];
struct info
{
    int maxn,sums;
}tr[N*4];
void update(int p)
{
    tr[p].maxn=max(tr[2*p].maxn,tr[2*p+1].maxn);
    tr[p].sums=tr[2*p].sums+tr[2*p+1].sums;
}
info operator+(const info& a,const info& b)
{
    return (info){max(a.maxn,b.maxn),a.sums+b.sums};
}
void build(int p,int l,int r)
{
    if(l==r)
    {
        tr[p].maxn=a[id[l]],tr[p].sums=a[id[l]];
        return ;
    }
    int mid=(l+r)/2;
    build(2*p,l,mid);
    build(2*p+1,mid+1,r);
    update(p);
}
void modify(int p,int l,int r,int pos,int val)
{
    if(l==r)
    {
        tr[p].maxn=val,tr[p].sums=val;
        return ;
    }
    //cout<<p<<endl;
    int mid=(l+r)/2;
    if(pos<=mid) modify(2*p,l,mid,pos,val); 
    else modify(2*p+1,mid+1,r,pos,val);
    update(p);
}
info query(int p,int l,int r,int ql,int qr)
{
    //cout<<p<<endl;
    if(ql==l&&qr==r)
    {
        return tr[p];
    }
    int mid=(l+r)/2;
    if(qr<=mid) return query(2*p,l,mid,ql,qr);
    else if(ql>mid) return query(2*p+1,mid+1,r,ql,qr);
    else return query(2*p,l,mid,ql,mid)+query(2*p+1,mid+1,r,mid+1,qr);
}
void dfs1(int u,int f)
{
    sz[u]=1;
    hs[u]=-1;
    fa[u]=f;
    dep[u]=dep[f]+1;
    for(auto v: e[u])
    {
        if(v==f) continue;
        dfs1(v,u);
        sz[u]+=sz[v];
        if(hs[u] == -1 || sz[hs[u]] < sz[v]) hs[u]=v;
    }
    //cout<<u<<endl;
}
void dfs2(int u,int t)
{
    l[u]=++tot;
    id[tot]=u;
    top[u]=t;
    if(hs[u]!=-1) dfs2(hs[u],t);
    for(auto v : e[u])
    {
        if(v==hs[u]||v==fa[u]) continue;
        dfs2(v,v);
    }
    r[u]=tot;
}
info check(int u,int v)
{
    info ans={(int)-1e9,0};
    while(top[u]!=top[v])
    {
        if(dep[top[u]]>dep[top[v]])
        {
            ans=ans+query(1,1,n,l[top[u]],l[u]);
            u=fa[top[u]];
        }
        else
        {
            ans=ans+query(1,1,n,l[top[v]],l[v]);
            v=fa[top[v]];
        }
        //cout<<v<<endl;
    }
    //cout<<"111"<<endl;
    if(dep[u]>dep[v]) ans=ans+query(1,1,n,l[v],l[u]);
    else ans=ans+query(1,1,n,l[u],l[v]);
    return ans;
}
void solve()
{
    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);
    }
    for(int i=1;i<=n;i++) cin>>a[i];
    cin>>q;
    dfs1(1,0);
    dfs2(1,1);
    build(1,1,n);
    //cout<<"ddddd"<<endl;
    while(q--)
    {
        string op;
        cin>>op;
        if(op[0]=='C')
        {
            int u,t;
            cin>>u>>t;
            modify(1,1,n,l[u],t);
        }
        else
        {
            int u,v;
            cin>>u>>v;
            info ans=check(u,v);
            if(op=="QMAX") cout<<ans.maxn<<endl;
            else cout<<ans.sums<<endl;
        }
    }

}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T;
    T=1;
    //cin>>T;
    while(T--)
     {
         solve();
     }
     return 0;
} 

Nauuo and Binary Tree

这是一道交互题,也是树剖的非传统应用。

题目大意

有一棵以 \(1\) 为根的二叉树,你可以询问任意两点之间的距离,求出每个点的父亲。

节点数不超过 \(3000\),你最多可以进行 \(30000\) 次询问。

解法

首先可以通过 \(n-1\) 次询问确定每个节点的深度。

然后考虑按深度从小到大确定每个节点的父亲,这样的话确定一个节点的父亲时其所有祖先一定都是已知的。

确定一个节点的父亲之前,先对树已知的部分进行重链剖分。

假设我们需要在子树 \(u\) 中找节点 \(k\) 所在的位置,我们可以询问 \(k\) 与 \(u\) 所在重链的尾端的距离,就可以进一步确定 \(k\) 的位置,具体见图:

其中红色虚线是一条重链,\(d\) 是询问的结果即 \(dis(k, bot[u])\),\(v\) 的深度为 \((dep[k]+dep[bot[u]]-d)/2\)。

这样的话,如果 \(v\) 只有一个儿子,\(k\) 的父亲就是 \(v\),否则可以递归地在 \(w\) 的子树中找 \(k\) 的父亲。

时间复杂度 \(O(n^2)\),询问复杂度 \(O(n\log n)\)。

具体地,设 \(T(n)\) 为最坏情况下在一棵大小为 \(n\) 的树中找到一个新节点的位置所需的询问次数,可以得到:

\[T(n)\le \begin{cases} 0&n=1\\ T\left(\left\lfloor\frac{n-1}2\right\rfloor\right)+1&n\ge2 \end{cases} \]

\(2999+\sum_{i=1}^{2999}T(i)\le 29940\),事实上这个上界是可以通过构造数据达到的,然而只要进行一些随机扰动(如对深度进行排序时使用不稳定的排序算法),询问次数很难超过 \(21000\) 次。

#include <algorithm>
#include <cstdio>
#include <iostream>

using namespace std;

const int N = 3010;

int n, fa[N], ch[N][2], dep[N], siz[N], son[N], bot[N], id[N];

int query(int u, int v) {
    printf("? %d %d\n", u, v);
    fflush(stdout);
    int d;
    scanf("%d", &d);
    return d;
}

void setFather(int u, int v) {
    fa[v] = u;
    if (ch[u][0])
    ch[u][1] = v;
    else
    ch[u][0] = v;
}

void dfs(int u) {
    if (ch[u][0]) dfs(ch[u][0]);
    if (ch[u][1]) dfs(ch[u][1]);

    siz[u] = siz[ch[u][0]] + siz[ch[u][1]] + 1;

    if (ch[u][1])
    son[u] = int(siz[ch[u][0]] < siz[ch[u][1]]);
    else
    son[u] = 0;

    if (ch[u][son[u]])
    bot[u] = bot[ch[u][son[u]]];
    else
    bot[u] = u;
}

void solve(int u, int k) {
    if (!ch[u][0]) {
    setFather(u, k);
    return;
    }
    int d = query(k, bot[u]);
    int v = bot[u];
    while (dep[v] > (dep[k] + dep[bot[u]] - d) / 2) v = fa[v];
    int w = ch[v][son[v] ^ 1];
    if (w)
    solve(w, k);
    else
    setFather(v, k);
}

int main() {
    int i;

    scanf("%d", &n);

    for (i = 2; i <= n; ++i) {
    id[i] = i;
    dep[i] = query(1, i);
    }

    sort(id + 2, id + n + 1, [](int x, int y) { return dep[x] < dep[y]; });

    for (i = 2; i <= n; ++i) {
    dfs(1);
    solve(1, id[i]);
    }

    printf("!");
    for (i = 2; i <= n; ++i) printf(" %d", fa[i]);
    printf("\n");
    fflush(stdout);

    return 0;
}
    ```



## 练习

[「洛谷 P3379」【模板】最近公共祖先(LCA)](https://www.luogu.com.cn/problem/P3379)(树剖求 LCA 无需数据结构,可以用作练习)

[「JLOI2014」松鼠的新家](https://loj.ac/problem/2236)(当然也可以用树上差分)

[「HAOI2015」树上操作](https://loj.ac/problem/2125)

[「洛谷 P3384」【模板】重链剖分/树链剖分](https://www.luogu.com.cn/problem/P3384)

[「NOI2015」软件包管理器](https://uoj.ac/problem/128)

[「SDOI2011」染色](https://www.luogu.com.cn/problem/P2486)

[「SDOI2014」旅行](https://hydro.ac/d/bzoj/p/3531)

[「POI2014」Hotel 加强版](https://hydro.ac/d/bzoj/p/4543)(长链剖分优化 DP)

[攻略](https://hydro.ac/d/bzoj/p/3252)(长链剖分优化贪心)

标签:剖分,int,top,DFS,树链,dep,重链,节点
From: https://www.cnblogs.com/prioritymygirl/p/18381541

相关文章

  • #8. 「模板」树链剖分
    题目传送门:#8.「模板」树链剖分、前置知识重链:重链(HeavyPath)是指树链剖分中的一条主要的路径,该路径上的节点数量较多,相邻节点之间的距离较近。轻链(LightPath)是指树链剖分中的其他路径,相邻节点之间的距离较远。LCA:最近公共祖先分析上树状数组首先,我们需要定义一个......
  • 重链剖分
    思想我先给出一些定义:定义一个结点的重子节点为其子节点中子树节点数最大的子节点,如有多个,随便取一个作为重儿子,如果没有子节点,就没有重儿子。定义一个结点的轻子节点为其除重子节点外的子节点。从这个节点到重子节点的边为重边,到其他子节点的边为轻边。由重边首位相连的链......
  • 算法总结-树链剖分
    算法总结-树链剖分概念重儿子(红点):父节点\(f\)的子节点中子树大小最大的儿子。轻儿子(蓝点):父节点\(f\)的子节点中除重儿子以外的节点。重链(橙圈框出的链):链顶为轻儿子,由重儿子组成的链。重链剖分用处:将树上任意一条路径划分成不超过\(\log{n}\)条连续的链(如实......
  • 对树链剖分的爱 题解
    前言题目链接:洛谷。题意简述给出一棵\(n\)个节点以\(1\)为根的有根树。对于第\(2\leqi\leqn\)个节点,其父亲\(f_i\)在\([l_i,r_i]\)中均匀随机。每个树的边有边权,初始为\(0\)。现在有\(m\)次操作,第\(i\)次操作表示将\((u_i,v_i)\)的路径上所有的边的权值统......
  • 算法学习笔记之树链剖分
    算法学习笔记之(熟练跑分)树链剖分PART1首先是第一部份,也就是熟练跑分最最最基础的用法——求\(LCA\)首先是树链剖分//图片出自董晓算法大概就是这样本质就是根据子树大小将一颗树剖分成若干条链然后更加方便地处理/加速处理信息所以直接上代码?不,还要证明树链剖......
  • 树链剖分
    具体见OI-wiki,下面是一些补充重链要求是极大的每个点都在某一个重链中,如果一个点是重子节点,那么其在与其父亲所连的边的重链中,否则在与其重子节点所连的边的重链中这一段的原因:我们走重链是不用关心的,因为同一重链的dfs序是连续的,我们可以用其他数据结构维护,我们只用关心这条......
  • 树链剖分
    前置知识时间戳在树的\(DFS\)中,以每个节点第一次被访问的顺序,依次给予\(N\)个点\(1-n\)的标记,通常用\(dfn[x]\)表示\(x\)号节点的时间戳。\(DFS\)序进行\(DFS\)时,对每个节点进入递归后以及回溯前各记录一次该点编号,最后产生\(2-n\)的序列即是\(DFS\)序,可设\(L[X]\)和\(R[X]\)......
  • Open3D 三维重建-Delaunay Triangulation (德劳内三角剖分)
    目录一、概述1.1原理1.2实现步骤1.3应用二、代码实现2.1关键函数2.2完整代码三、实现效果3.1原始点云3.2重建后点云Open3D点云算法汇总及实战案例汇总的目录地址:Open3D点云算法与点云深度学习案例汇总(长期更新)-CSDN博客一、概述        德劳内三角剖......
  • 树链剖分
    定义把树剖成一条条不相交的链,对树的操作就转化成了对链的操作概念重儿子:对于每一个非叶子节点,它的儿子中以那个儿子为根的子树节点数最大的儿子为该节点的重儿子轻儿子:对于每一个非叶子节点,它的儿子中非重儿子的剩下所有儿子即为轻儿子重边:连接任意两个重儿子的边叫做重......
  • MATLAB生成各类区域网格剖分
    一、双洞模型代码:hg=[1111111120-20010-10-20210-10020-2012120-2012101111000000001111000000000000111122221111];ug=[1111010-1......