线段树学习笔记
线段树
线段树的核心思想是分而治之,简单来说就是把复杂的大问题转化成一些容易解决的小问题。
将这个思路用在区间中,就是把一个大的区间拆解为几个小的区间。
线段树非常适合解决区间和、区间最大值、区间最小值等问题
线段树的构建
假如我们有六个元素3 20 11 19 6 2,整个数组的区间是 \([1, 6]\),这是线段树的根节点,基于上面的思想,我们将大区间递归分解为更小的区间,直到区间中只包含一个元素。
区间 \([1, 6]\) 有两个子节点,分别是 \([l, mid]\) 和 \([mid + 1, r]\),也就是 \([1, 3]\) 和 \([4, 6]\)。
区间 \([1, 3]\) 也有两个子节点,分别是 \([1, 1]\) 和 \([2, 3]\)。
\([1, 1]\) 只包含一个元素,所以此节点为叶子节点,没有儿子。
\([2, 3]\) 可以分为区间 \([2, 2]\) 和 \([3, 3]\)。
\([2, 2]\) 和 \([3, 3]\) 为叶子节点,和\([1, 1]\) 同理。
把区间 \([4, 6]\) 按照同样的方式分解。
就是不给你代码=-=
线段树的区间修改
线段树的优势就是区间修改和区间查询了。
如果想要修改一个区间,就要找到所有包含此区间任何一个数的值的节点去修改。
那么我们可以对于每一个节点,进行以下的操作:
- 如果它的值包含我们修改的区间,则对它的左子树和右子树进行同样的操作,操作完成后更新这个节点的值为左子树 + 右子树的值
- 如果他的值完全不包含我们需要进行修改的区间,则返回
- 如果他完全包含需要修改的区间,则修改其值以及其的所有子节点,因为它的子节点也一定完全包含该区间,返回
但是我们发现修改完全包含需要修改的区间的每一个子节点时间复杂度为 \(O(n logn)\) ,这比数组直接暴力修改还慢了(恼)。
我们可以发现直接将子节点修改没有什么意义,查询的时候不一定可以访问到每一个子节点,这就很浪费。
可以使用一个懒标记来优化修改的时间复杂度。
懒标记顾名思义很懒的标记,在收到区间修改操作时不会立即将子节点修改,而是将要修改的值存进懒标记中。
在区间查询的时候(需要用到子节点的时候)再将两个儿子节点修改,并给儿子节点的懒标记加上当前节点的懒标记,注意清空当前节点的懒标记。
听不懂?让我们说人话
领导给你布置了一项任务,让你传递给你的下属,而你是一个很懒的人,你当然想偷懒而不被领导发现。
于是你想到了一个方法:领导给你布置任务时,你先将任务存起来不做,等领导来查工作情况时再把工作一下子发给你的下属,当然你的下属也会这么干。
代码:
bool InRange(int l, int r, int L, int R) {
//判断[L, R]内是否完全包含[l, r]
return (l >= L) && (r <= R);
}
bool OutofRange(int l, int r, int L, int R) {
//判断区间[l, r]和[L, R]是否完全不重合
return (r < L) || (l > R);
}
void maketag(int u, int len, int x) {
lzy[u] += x;
//更新懒标记
w[u] += len * x;
//更新区间和
}
void pushup(int u) {
w[u] = w[u * 2] + w[u * 2 + 1];
//更新区间和
}
void update(int u, int l, int r, int L, int R, int x) {
if(InRange(l, r, L, R)) {
maketag(u, r - l + 1, x);
//如果完全包含,则修改此区间
return ;
}
if(OutofRange(l, r, L, R)) {
return ;
}
int mid = (l + r) >> 1;
update(u * 2, l, mid, L, R, x);
update(u * 2 + 1, mid + 1, r, L, R, x);
//遍历左子树和右子树
pushup(u);
}

浙公网安备 33010602011771号