1 线段树
1.1 什么是线段树
线段树是一种二叉搜索树,它的每个节点保存一条线段(即数组的一段子数组)
1.2 作用
用于高效解决连续区间的动态查询问题
1.3 特点
- 时间复杂度为O(logN)
- 未优化的空间复杂度为2N
1.4 节点
线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间。例如父亲的区间是[ a , b ],那么 c = ( a + b ) / 2 左儿子的区间是 [ a , c ],右儿子的区间是[ c + 1 , b ]。
1.5 延迟标记
区间更新是指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(logn),例如当我们要更新区间[0,3]内的叶子节点时,需要更新除了叶子节点3,9外的所有其他节点。为此引入了线段树中的延迟标记概念,这也是线段树的精华所在。
延迟标记:每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。
引用自《一步一步理解线段树》
2 示例
给定一个数组 {1,5,2,7,8,0,6,9},数组长度为 8,数据类型为 int,将其存储在一棵线段树中。
注意:节点表示的区间指的是数组下标,而非数组中存储的值
其中:
黑色数字代表节点编号
红色数字代表数组下标
蓝色数字代表存储的数据内容
可以看到节点编号与数组下标之间差为固定值 7,对于长度为 n 的数组,这个差值为固定值 n-1,子节点与父节点的节点编号也存在关系(若一个节点编号为 x,则其左儿子编号为 2x,右儿子编号为 2x+1)。然而,在实际建树过程中,可能由于采用不是广度遍历的方式,节点编号与图中顺序不符,节点编号与数组下标很可能不固定,子节点与父节点编号的关系也不确定,要根据具体实现方式确定,或者直接在节点内存储左右儿子的节点编号,以便查询或修改时进行遍历。
3 用线段树解决实际问题
3.1 问题描述
农民约翰试图通过让他们玩智力玩具让奶牛保持敏锐。较大的玩具之一是谷仓里的灯。N( 2 ± N × 100000 )个畜栏依次编号为 1…N,上方有都一盏色彩斑斓的灯。
傍晚时分,所有的灯都熄灭了。奶牛用一组按钮开关( N 个)控制灯的开关,按开关 i 可以改变编号为 i 的灯的状态,从关到开或从开到关。
奶牛读取并执行 m( 1 ± m × 100000 )个操作的列表,每个操作用 0 或 1 表示。
操作一(用 0 表示)包括两个整数 Si 和 Ei( 1 ≤ Si ≤ Ei ≤ N ),要求奶牛把范围在 Si 到 Ei 的开关全部按一次。
操作二(用 1 表示)要求奶牛计算两个整数 Si 和 Ei( 1 ≤ Si ≤ Ei ≤ N )给出的范围内有多少盏灯亮着。
帮助 FJ 确保奶牛通过处理列表并产生正确的计数得到正确的答案。
3.2 输入&输出
输入
第 1 行:两个空间分离的整数 n 和 m
第 2 行…M + 1:每行有 3 个整数,代表一次操作,三个整数分别为:操作、Si 和 Ei。
输出
第 1 行…查询数:对于每一个查询操作,在单行上打印亮着的灯的数目。
示例
n = 4,m = 5。对于每一盏灯,O 代表关,* 代表开
初始:O O O O
0 1 2 –>* * O O , 改变灯 1 到 2 的状态
0 2 4 –>* O * *
1 2 3 –>1 , 计算灯 2 到 3 中亮着的数目
0 2 4 –>* * O O
1 1 4 –>2
3.3 代码
节点定义
package r4.p2525;
class Node {
int value; //叶子节点:1表示灯亮着,0表示灯不亮;非叶子节点:表示亮着的灯的数目
int l; //节点表示的区间左边界
int r; //节点表示的区间右边界
int lc; //左儿子的节点编号
int rc; //右儿子的节点编号
int tag; //延迟标记,记录节点是否被修改
Node(int l, int r, int value, int tag) {
this.l = l;
this.r = r;
this.value = value;
this.tag = tag;
}
}
线段树实现
package r4.p2525;
class SegTree {
private Node[] nodes; //节点数组
private int total = 0; //目前节点总数
SegTree(int size) {
nodes = new Node[4 * size + 1];
}
/**
* 建立线段树或其子树(对应数组下标为l~r)
*
* @param l 范围左边界
* @param r 范围右边界
*/
void build(int l, int r) {
total++;
int now = total;
nodes[total] = new Node(l, r, 0, 0);
if (l == r) {
return;
}
int mid = (l + r) / 2;
nodes[now].lc = total + 1;
build(l, mid);
nodes[now].rc = total + 1;
build(mid + 1, r);
}
/**
* 修改l~r范围内的数组值(使用延迟标记)
*
* @param l 范围左边界
* @param r 范围右边界
* @param nodeIndex 要修改的节点编号
*/
void change(int l, int r, int nodeIndex) {
int nodeL = nodes[nodeIndex].lc;
int nodeR = nodes[nodeIndex].rc;
if (nodes[nodeIndex].l == l && nodes[nodeIndex].r == r) {
changeValue(nodeIndex);
changeTag(nodeIndex);
} else {
if (nodes[nodeIndex].tag == 1) { //如果节点修改了,则修改其所有子节点
update(nodeIndex);
}
int mid = getMid(nodeIndex);
if (mid >= r) {
change(l, r, nodeL);
} else if (mid < l) {
change(l, r, nodeR);
} else {
change(l, mid, nodeL);
change(mid + 1, r, nodeR);
}
nodes[nodeIndex].value = nodes[nodeL].value + nodes[nodeR].value;
}
}
/**
* 查询l~r范围内的亮灯数
*
* @param l 范围左边界
* @param r 范围右边界
* @param nodeIndex 要查询的节点编号
* @return
*/
int query(int l, int r, int nodeIndex) {
if (nodes[nodeIndex].l == l && nodes[nodeIndex].r == r) {
return nodes[nodeIndex].value;
}
int nodeL = nodes[nodeIndex].lc;
int nodeR = nodes[nodeIndex].rc;
if (nodes[nodeIndex].tag == 1) {
update(nodeIndex);
}
int mid = getMid(nodeIndex);
if (mid >= r) {
return query(l, r, nodeL);
} else if (mid < l) {
return query(l, r, nodeR);
} else {
return query(l, mid, nodeL) + query(mid + 1, r, nodeR);
}
}
/**
* 去除节点的延迟标记,并修改其子节点
*
* @param nodeIndex 节点编号
*/
private void update(int nodeIndex) {
int nodeL = nodes[nodeIndex].lc;
int nodeR = nodes[nodeIndex].rc;
nodes[nodeIndex].tag = 0;
changeTag(nodeL);
changeTag(nodeR);
changeValue(nodeL);
changeValue(nodeR);
}
/**
* 获取区间的中间值
*
* @param nodeIndex 节点编号
* @return 中间值
*/
private int getMid(int nodeIndex) {
return (nodes[nodeIndex].l + nodes[nodeIndex].r) / 2;
}
/**
* 把某个节点范围内所有灯的开关按一遍
* 此时值为1的叶子节点数目为未按开关前值为0的叶子节点数目
* 即值为1的叶子节点数目=所有叶子节点个数-值为1的叶子节点数目=当前节点范围-当前节点亮着的灯的数目
*
* @param nodeIndex
*/
private void changeValue(int nodeIndex) {
nodes[nodeIndex].value = nodes[nodeIndex].r - nodes[nodeIndex].l + 1 - nodes[nodeIndex].value;
}
private void changeTag(int nodeIndex) {
nodes[nodeIndex].tag = (nodes[nodeIndex].tag + 1) % 2;
}
}
主类
package r4.p2525;
import java.util.Scanner;
public class Main {
public static void main(String args[]) {
Scanner scanner = new Scanner(System.in);
int lightsNum = scanner.nextInt();
int operationsNum = scanner.nextInt();
SegTree segTree = new SegTree(lightsNum);
segTree.build(1, lightsNum);
for (int i = 0; i < operationsNum; i++) {
int operation = scanner.nextInt();
int start = scanner.nextInt();
int end = scanner.nextInt();
if (operation == 0) {
segTree.change(start, end, 1);
} else {
int c = segTree.query(start, end, 1);
System.out.println(c);
}
}
}
}
输入
4 5
0 1 2
0 2 4
1 2 3
0 2 4
1 1 4
输出
1
2