线段树学习笔记

请注意,本文默认大家学过单改区查的基础线段树(包括动态开点),若未学,请移步 OI-wiki,例题可以使用树状数组的题目。

区间修改:懒标记(lazy-tag)

【模板】线段树 1

题目描述

已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 \(k\)
  2. 求出某区间每一个数的和。

输入格式

第一行包含两个整数 \(n, m\),分别表示该数列数字的个数和操作的总个数。

第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(m\) 行每行包含 \(3\)\(4\) 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 \([x, y]\) 内每个数加上 \(k\)
  2. 2 x y:输出区间 \([x, y]\) 内每个数的和。

输出格式

输出包含若干行整数,即为所有操作 2 的结果。

样例输入 #1

1
2
3
4
5
6
7
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

样例输出 #1

1
2
3
11
8
20

提示

对于 \(100\%\) 的数据:\(1 \le n, m \le {10}^5\)

很明显,如果用暴力进行区间修改,不能通过。

那么引入 lazy-tag。

首先,先介绍 lazy-tag 的概念:

对于每个有 lazy-tag 的节点,在只有需要用到自己的子节点时才会把修改的命令(lazy-tag)交给子节点。

lazy-tag 用一个叫做 push_down 的操作下放修改操作。

1
2
3
4
5
6
7
8
9
void p_d_(long long u)
{
long long L=u<<1,R=u<<1|1;
t[L].sum+=t[u].y*(t[L].r-t[L].l+1);//处理左子树总和
t[R].sum+=t[u].y*(t[R].r-t[R].l+1);//右子树
t[L].y+=t[u].y;//标记下放
t[R].y+=t[u].y;//同上
t[u].y=0;//清零,代表下放结束
}

其中,y 就是 lazy-tag。

第二,up_date 函数在符合 t[u].l>=l&&t[u].r<=r 时,将 lazy-tag 加上此次加操作的值,并增加此区间总和。

若不符合,则进行 push_down 操作,为递归左右子树铺垫。

第三,query 函数在 t[u].l>=l&&t[u].r<=r 不成立时,也需进行 push_down 操作,原因同上。

显然,此时复杂度为 \(O(mlogn)\)

权值线段树

普通的线段树,记录的是数组中当前区间的各种信息。

权值线段树,则记录的是每种数据出现的次数。

如:数组为 1 1 4 5 1 4

则其出现次数为(从 \(0\) 开始)0 3 0 0 2 1

线段树合并

前置知识:权值线段树(一般线段树合并合并的是权值线段树)。

线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。它常常被用于维护树上或是图上的信息。——OI wiki

如果每次合并都新开一棵满树,时间、空间都无法接受。

那么,就需要本文开头提到的前置知识动态开点。

设需要合并的两棵树分别叫 \(T1\)\(T2\)

从上往下(即从 \(1\) 号节点开始递归向下),对于每个节点,若 \(T1\) 上无此节点,就只加入 \(T2\) 上的此节点。反之亦然。

如果都有此节点,若是叶子节点,直接合并所有信息。

若否,通过子节点的返回(返回左儿子、右儿子)更新。

OI-wiki 上对其时间复杂度的证明不严谨,读者可以在写的时候自己证其时间复杂度。

扫描线

下面这张动图,诠释了这种算法在处理面积并上的的大致思路:(感谢 OI-wiki,面积并处理部分也请参见 OI-wiki)

本博客则讲述其解决周长并上的解法,详见T7

例题

T1

首先的思路是暴力(二维前缀和),然而会时间和空间超限。

优化:因为“星星按y坐标增序给出,y坐标相同的按x坐标增序给出”,所以只需查找从 \(0\) 高度到此高度范围内共有多少颗星星(因为保证 \(y\) 增序了),求和即可。

T2

很明显,因为是“区查单改”且是较为特殊的异或,我们需要把“异或次数”作为 lazy-tag。

同时因为只有 \(1\)\(0\),所以 lazy-tag 每次异或(即 \(+1\bmod 2\))即可。

T3

当然,“开方”操作的懒标记以及修改都很难做。

然而,我们发现:\(\Bigg \lfloor \sqrt{\sqrt{\sqrt{\sqrt{\sqrt{\sqrt 10^{12}}}}}} \Bigg \rfloor=1\)

所以就这样操作即可:不是全 \(0\) 或全 \(1\),暴力;是,不管。

T4

我们需要两个 lazy-tag,一个是乘的,一个是加的。

优先级是乘 \(>\) 加,因为加完再乘,加的那部分也要被乘,反之不然。

T5

工厂

小明的叔叔是一家工厂的厂长。叔叔的工厂有 n 个车间,编号为 1~n。

管理工厂是很麻烦的事情,特别是在多次调整机器以及员工之后,统计总生产量更是难事。

第 i 个车间在刚开始的时候机器生产力为 ai,有 bi 个员工,那么这个车间的生产力就为ai*bi。

工厂的总生产力定义为所有车间的生产力之和。

接下来的 m 天,每天叔叔就会调整一段区间的车间。

有两种调整:

第一种,是对于一段区间[l,r]的每一个车间重新分配每个车间的工人数为 x。

第二种,是对于一段区间[l,r]的每一个车间增加机器生产力 x。

现在,小明的叔叔想知道每天调整之后工厂的生产量变为多少。

我们发现,这和 T4 很像,只是“加”变为了“覆盖”。

于是把“加” lazy-tag 变为“覆盖”的即可。

T6

权值线段树 \(+\) 线段树合并。

每个子树中的颜色,开一颗权值线段树,向上回溯时合并即可。

T7

首先,需要离散化,而后处理时有两种思路:

用一棵线段树,所以有一点点思维难度。

从上向下扫描,每次遇到一条边,若为上边,则在线段树上此区间进行加操作,下边则在此区间进行减操作。与面积并时相似。这是横边。竖边则利用扫描线走过的距离乘横向距离即可。

可以参考这篇题解

思路一的思维难度在于,竖边的处理较为麻烦,那么我们就如法炮制,竖边也建一棵线段树处理。

小结

其实线段树还有不少应用,限于篇幅只能列出一部分。

赛中一般不会出裸线段树,所以想到用线段树是一个重要环节。

剩余部分可以参见 OI-wiki,讲的很清晰。有时间或有需求也可以学习几种线段树变形(李超等)。

另,由于部分题目代码过于长或因为某题最好自己写代码以学习,所以我没有放。

致谢

我学校的信息学教练 zxc 以及不是我学校的一位教练 gtc,为此文提供了例题以及基础;

OI-wiki 线段树部分编写人员及 OI-wiki 建设者,为本文线段树合并部分提供了定义以及为扫描线提供动图讲解;

我之前的英语老师 Cally,为本博客风格提供建议;

同学 lqs、whl,为我改代码(T5,lqs),讲解 T4(whl);

hexo、github以及 next,为本博客提供支持;

同学 lyh,帮我调弄 hexo;

洛谷,提供题目、上传图片、教会我 \(\LaTeX\)

\(mathjax\),对本文提供 \(\LaTeX\) 支持。