📅  最后修改于: 2023-12-03 15:11:38.114000             🧑  作者: Mango
细分树,也叫做可持久化线段树,是一种数据结构,用于对一个数列进行多次询问,每次询问都要求某个区间的统计信息,如区间和、区间最大值、区间最小值等等。与普通线段树不同之处在于,细分树可以在每次修改数列时,将原来的数列不变,并生成一个新的数列,这样就可以在之前的数列中查询到之后的数列的信息。
在本文中,我们将介绍如何使用细分树来实现一种常见的需求,即计算一个数列中给定范围的总和。
假设有一个长度为 N
的数列 a
,我们需要支持以下两种操作:
add(x, v)
:将 a[x]
的值增加 v
。sum(l, r)
:查询区间 [l, r]
的总和。构造细分树后,我们可以对于原来的数列 a
进行多次修改和查询,并且每次修改和查询都不会影响原来的数列。
我们首先需要定义细分树中的节点结构,以及节点信息维护函数。
struct node {
int l, r;
ll sum;
} tr[N * 40];
int root[N], idx = 0;
inline void pushup(int p) {
tr[p].sum = tr[tr[p].l].sum + tr[tr[p].r].sum;
}
这里我们使用一个结构体 node
存储每个节点的信息。l
和 r
表示这个节点所表示区间的左右端点,sum
表示这个节点所表示区间的总和。root
数组则表示所有版本的根节点,idx
用于维护 tr
数组的下标。
节点的信息维护函数 pushup
则表示如何计算一个节点的信息。对于一个节点而言,它的信息可以从它的左右孩子节点推出来。在本例中,节点的信息为区间和,因此节点的信息维护函数将它的 左右孩子节点 对应的 区间和 相加,得到了该节点所表示的 区间的区间和。
对于每一个版本的根节点,我们都可以对其进行修改操作。因此,修改操作实际上就是将原来的节点拷贝一份,然后将需要修改的部分进行修改即可。
void modify(int l, int r, int &p, int q, int x, int v) {
p = ++idx;
tr[p] = tr[q];
if (l >= x && r <= x) tr[p].sum += v;
else {
int mid = l + r >> 1;
if (x <= mid) modify(l, mid, tr[p].l, tr[q].l, x, v);
else modify(mid + 1, r, tr[p].r, tr[q].r, x, v);
pushup(p);
}
}
如上,修改操作表示为 modify
函数。该函数需要传递一些参数:
l
和 r
表示当前节点对应的区间。p
和 q
分别表示当前节点所在的版本的下标和之前一个版本的下标。x
和 v
分别表示需要修改的元素下标和修改的值。该函数的实现方式与普通的线段树实现方式基本一致。我们首先将当前节点的值拷贝到新的节点中,然后再对需要修改的部分进行修改。如果当前节点所表示区间包含了需要修改的元素,则将节点的总和加上修改的值,否则递归地对左右子节点进行修改,并且在修改完毕后更新总和。
查询操作实际上就是计算出一个区间 [l,r]
的总和。由于我们在维护线段树的时候已经计算出了每个节点的总和,因此可以利用这些值来加快查询的速度。
ll query(int l, int r, int p, int x, int y) {
if (l >= x && r <= y) return tr[p].sum;
ll res = 0;
int mid = l + r >> 1;
if (x <= mid) res += query(l, mid, tr[p].l, x, y);
if (y > mid) res += query(mid + 1, r, tr[p].r, x, y);
return res;
}
如上,查询操作表示为 query
函数。该函数需要传递一些参数:
l
和 r
表示当前节点对应的区间。p
表示需要查询的版本的下标。x
和 y
分别表示需要查询的区间的左右端点。该函数的实现方式与普通的线段树实现方式基本一致。我们首先判断当前节点所表示的区间是否在需要查询的区间范围内,如果是,则返回该节点的总和。否则递归地对左右子节点进行查询,并且将两个子节点的查询结果相加,然后返回该值。
细分树是一种非常有用的数据结构,可以用来解决多次查询、修改同一个数列的问题。本文介绍了如何使用细分树来实现一种常见的需求,即计算一个数列中给定范围的总和。学习了本文,您将能够更深刻地理解细分树的实现方式,并且根据自己的需要来拓展实现更多的功能。