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

树链剖分详解

时间:2023-08-19 14:44:53浏览次数:42  
标签:剖分 int top 树链 dfn 重链 节点 详解

目录


前言

在同学们一路走来的过程中,一定已经学习了倍增求 LCA 的算法。

倍增求 LCA 算法只适用于少部分情况,那么,如果要求在求出 LCA 的同时,对两点 \(a, b\) 之间的所有点权(或边权)进行求和或修改,又该怎么做呢?这里介绍一种 树链剖分 的方法(树链剖分有多种,这里只介绍其中用途最广的一种,重链剖分)。


一、树剖是什么?

顾名思义,树链剖分就是将整棵树剖分为若干条链,使它组合成一个线性结构,然后用其他的数据结构维护树上的信息。

重链剖分 可以将树上的任意一条路径划分成不超过 \(O(\log n)\) 条连续的链,保证划分出的每条链上的节点 DFS 序 连续,因此可以方便地使用 线段树 之类的数据结构来维护树上的信息。

二、重链剖分

首先,我们要明确一些定义:

  • 重子节点:某个点的子节点中子树最大的子结点。如果有多个子树最大的子结点,取其中任意一个即可。如果该点没有子节点,就无重子节点。

  • 轻子节点:除重子节点以外的所有子结点。

  • 重边:从节点到重子节点的边。

  • 轻边:从节点到轻子节点的边。

  • 重链:由若干条首尾衔接的重边构成的链。

若我们把无重子节点的点也当成一条重链,则这棵树就可以被划分成若干条互不相交的重链。容易发现,一颗子树内的 DFS 序是连续的。这也方便了我们维护字树内的值。

其实有一种树剖方试叫轻链剖分,划分的方法与重链剖分类似,这里不多赘述。

注:图片引自 OI-WIKI。

树剖的实现

重剖的实现是由两个 DFS 完成的。

对于每个节点 \(u\):

  • 用第一个 DFS 记录每个结点的父节点(记作 \(f_u\))、深度(记作 \(dep_u\))、子树大小(记作 \(son_u\))、重子节点(记作 \(heavy_u\))。
  • 第二个 DFS 则记录所在链的头(记作 \(top_u\))、按先重边后轻边的顺序遍历时的 DFS 序(记作 \(dfn_u\))、DFS 序对应的节点编号(由于 C++ 中 \(\texttt{rank}\) 为关键字,记作 \(ranki_u\))。显然,有 \(rank_{dfn_u}=u\)。

修改部分:

  • 对于每次两点之间路径上的点的区间修改或查询操作,将两个点沿着重链不断向上跳祖先,由于每条链内的点的 DFS 序一定是连续的,所以区间修改/查询 \(dfn_{top_x} \sim dfn_x\) 即可。
  • 对于以某个顶点为根的子树的修改操作,由于这棵子树内的 DFS 序连续(上文已说明),区间修改/查询 \(dfn_x \sim dfn_x+son_x-1\) 即可。
  • 使用 线段树 维护修改标记即可。

模板题代码(洛谷 P3384 【模板】重链剖分/树链剖分):

// Problem: P3384 【模板】重链剖分/树链剖分
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3384
// Memory Limit: 128 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 100005;

int n, m, r, p, tot, opt, x, y, z, cnt;
int heavy[N], son[N], dfn[N], top[N], ranki[N], val[N], dep[N], f[N], head[N];

struct edge
{
	int to, nxt;
}e[N << 1];

struct node
{
	int l, r, tag, sum;
}tree[N << 2];

inline void add_edge(int x, int y)
{
	e[++tot] = {y, head[x]}, head[x] = tot;
}

inline void dfs1(int u, int fa)
{
	dep[u] = dep[fa] + 1, f[u] = fa, son[u] = 1;
	for(int i = head[u]; ~i; i = e[i].nxt)
	{
		int v = e[i].to;
		if(v == fa) continue;
		dfs1(v, u);
		son[u] += son[v];
		if(son[v] > son[heavy[u]]) heavy[u] = v;
	}
	return;
}

inline void dfs2(int u, int tp)
{
	top[u] = tp, dfn[u] = ++cnt, ranki[dfn[u]] = u;
	if(!heavy[u]) return;
	dfs2(heavy[u], tp);
	for(int i = head[u]; ~i; i = e[i].nxt)
	{
		int v = e[i].to;
		if(v != f[u] && v != heavy[u]) dfs2(v, v);
	}
	return;
}

inline void push_up(int x)
{
	tree[x].sum = tree[x << 1].sum + tree[x << 1 | 1].sum;
}

inline void push_down(int x)
{
	tree[x << 1].tag += tree[x].tag, tree[x << 1].tag %= p;
	tree[x << 1 | 1].tag += tree[x].tag, tree[x << 1 | 1].tag %= p;
	tree[x << 1].sum = (tree[x << 1].sum + tree[x].tag * (tree[x << 1].r - tree[x << 1].l + 1)) % p;
	tree[x << 1 | 1].sum = (tree[x << 1 | 1].sum + tree[x].tag * (tree[x << 1 | 1].r - tree[x << 1 | 1].l + 1)) % p;
	tree[x].tag = 0;
}

inline void build(int l, int r, int x)
{
	tree[x] = {l, r, 0, 0};
	if(l == r) return (void) (tree[x].sum = val[ranki[l]] % p);
	int mid = l + r >> 1;
	build(l, mid, x << 1);
	build(mid + 1, r, x << 1 | 1);
	push_up(x);
}

inline void update(int l, int r, int k, int x)
{
	if(l <= tree[x].l && tree[x].r <= r)
		return (void) (tree[x].tag = (tree[x].tag + k) % p, tree[x].sum = (tree[x].sum + k * (tree[x].r - tree[x].l + 1)) % p);
	int mid = tree[x].l + tree[x].r >> 1;
	push_down(x);
	if(l <= mid) update(l, r, k, x << 1);
	if(r > mid) update(l, r, k, x << 1 | 1);
	push_up(x);
}

inline int query(int l, int r, int x)
{
	if(l <= tree[x].l && tree[x].r <= r) return tree[x].sum;
	int mid = tree[x].l + tree[x].r >> 1, ans = 0;
	push_down(x);
	if(l <= mid) ans += query(l, r, x << 1);
	if(r > mid) ans += query(l, r, x << 1 | 1);
	push_up(x);
	return ans % p;
}

inline void change(int x, int y, int z)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) swap(x, y);
		update(dfn[top[x]], dfn[x], z, 1);
		x = f[top[x]];
	}
	if(dfn[x] > dfn[y]) swap(x, y);
	update(dfn[x], dfn[y], z, 1);
}

inline int ask(int x, int y)
{
	int ans = 0;
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) swap(x, y);
		ans = (ans + query(dfn[top[x]], dfn[x], 1)) % p;
		x = f[top[x]];
	}
	if(dfn[x] > dfn[y]) swap(x, y);
	return (ans + query(dfn[x], dfn[y], 1)) % p;
}

signed main()
{
	ios :: sync_with_stdio(false);
	memset(head, -1, sizeof head);
	cin >> n >> m >> r >> p;
	for(int i = 1; i <= n; i++) cin >> val[i];
	for(int i = 1; i < n; i++)
	{
		cin >> x >> y;
		add_edge(x, y);
		add_edge(y, x);
	}
	dfs1(r, 0);
	dfs2(r, 0);
	build(1, n, 1);
	while(m--)
	{
		cin >> opt;
		if(opt == 1)
		{
			cin >> x >> y >> z;
			change(x, y, z);
		}
		else if(opt == 2)
		{
			cin >> x >> y;
			cout << ask(x, y) % p << '\n';
		}
		else if(opt == 3)
		{
			cin >> x >> z;
			update(dfn[x], dfn[x] + son[x] - 1, z, 1);
		}
		else if(opt == 4)
		{
			cin >> x;
			cout << query(dfn[x], dfn[x] + son[x] - 1, 1) % p << '\n';
		}
	}
	return 0;
}

例题

\(\text{\color{3498DB}洛谷 P3384 【模板】重链剖分/树链剖分}\)
\(\text{\color{3498DB}洛谷 P2590 [ZJOI2008] 树的统计}\)
\(\text{\color{52C41A}洛谷 P3128 [USACO15DEC] Max Flow P}\)
\(\text{\color{3498DB}洛谷 P3038 [USACO11DEC] Grass Planting G}\)
\(\text{\color{3498DB}洛谷 P3833 [SHOI2012] 魔法树}\)
\(\text{\color{3498DB}洛谷 P4114 Qtree1}\)
\(\text{\color{3498DB}洛谷 P2146 [NOI2015] 软件包管理器}\)
\(\text{\color{3498DB}洛谷 P4281 [AHOI2008] 紧急集合 / 聚会}\)
\(\text{\color{9D3DCF}洛谷 P1505 [国家集训队] 旅游}\)


总结

以上就是树链剖分的内容,本文仅仅简单介绍了重链剖分的思路与实现流程,而重链剖分能解决的问题还远不止于此。

标签:剖分,int,top,树链,dfn,重链,节点,详解
From: https://www.cnblogs.com/Heartquakes/p/17642451.html

相关文章

  • 【愚公系列】2023年08月 WPF控件专题 Button控件详解
    (文章目录)前言WPF控件是WindowsPresentationFoundation(WPF)中的基本用户界面元素。它们是可视化对象,可以用来创建各种用户界面。WPF控件可以分为两类:原生控件和自定义控件。原生控件是由Microsoft提供的内置控件,如Button、TextBox、Label、ComboBox等。这些控件都是WPF中常见......
  • ThreadLocal 详解
    ThreadLocal中ThreadLocalMap的数据结构?Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLo......
  • thingsboard gateway mqtt 连接详解
    mqtt的配置可见官网说明:https://thingsboard.io/docs/iot-gateway/config/mqtt/ 这里主要从源码说一下tbgateway里,mqttconnector的启动过程,和mqttconnector怎么工作  mqttconnector实现消息处理,主要在几个回调方法上,下面就主要方法说明:   接收消息后,就是处理......
  • Optional详解
    1.介绍Optional是Java8引入的一个新的类,它是java.util包下面的一个类。主要目的是为了解决空指针异常问题,它既可以含有对象也可以为空。2.Optional的使用2.1:创建一个Optional如果需要创建一个空的Optional的话,则可以使用Optional的empty()方法。empty方法的代码为:publicsta......
  • 技术分享| WebRTC之SDP详解
    一,什么是SDPWebRTC是WebReal-TimeCommunication,即网页实时通信的缩写,是RTC协议的一种Web实现,项目由Google开源,并和IETF和W3C制定了行业标准。WebRTC是点对点通讯,他的通话建立需要交换媒体信息才能建立,媒体信息的载体就是SDP。SDP(SessionDescriptionProtocol)是......
  • 融媒行业落地客户旅程编排,详解数字化用户运营实战
    移动互联网时代是流量红利的时代,企业常用低成本的方式进行获客,“增长黑客”的概念大范围传播。与此同时,机构媒体受到传播环境的影响,也开始启动全行业的媒体融合转型。在此背景下,2015年神策数据成立,核心解决的是帮助客户通过数据分析实现更好的增长。2020年之后数字化转型的大趋势......
  • 软件测试|Linux三剑客之grep命令详解
    简介grep是一款在Linux和类Unix系统中广泛使用的文本搜索工具。它的名字来源于GlobalRegularExpressionPrint(全局正则表达式打印),它的主要功能是根据指定的模式(正则表达式)在文本文件中搜索并打印匹配的行。grep非常强大且灵活,可以用于日志分析、文件过滤、代码搜索等多种场......
  • 软件测试|Linux三剑客之sed命令详解
    简介sed(StreamEditor)是一款流式文本编辑器,在Linux和类Unix系统中广泛使用。它的设计目的是用于对文本进行处理和转换,可以用于替换、删除、插入、打印等操作。sed命令通过逐行处理文本,允许您使用简单的命令来编辑大量文本数据。本文将详细介绍sed命令的基本用法和一些常见的......
  • 鸿蒙万能卡片开发详解-记忆翻牌游戏
    (目录)1.前言      翻牌游戏万能卡片,随机生成16张共包含8张完全不同的图像,游戏的目标是在有限30秒时间内,将16张卡片中包含相同的图像的卡片两两配对。匹配的规则是连续点击两张卡片,若卡背面的图像相同,则匹配成功,若不同则配对失败。游戏主要考察玩家的记忆力,因为游戏还规定......
  • JS数据类型详解
    JS的数据类型分为基本数据类型+引用数据类型基本数据类型:number,boolean,string,null,undefined, symbol(独一无二并且不可变的数据类型),bigint引用数据类型: Function,Array,Object区别:基本数据类型由于所占内存大小可控所以放于栈中,引用数据类型所占空间不固定放于堆中,并生......