1.概述
线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,它基本能保证每个操作的复杂度为O(lgN)。
2.线段树基本操作
线段树的基本操作主要包括构造线段树,区间查询和区间修改.
3.线段树的相关操作代码
(1) 线段树构造
首先介绍构造线段树的方法:让根节点表示区间[0,N-1],即所有N个数所组成的一个区间,然后,把区间分成两半,分别由左右子树表示。不难证明,这样的线段树的节点数只有2N-1个,是O(N)级别的,如图:
//构造求解区间最小值的线段树
function 构造以v为根的子树
ifv所表示的区间内只有一个元素
v区间的最小值就是这个元素, 构造过程结束
endif
把v所属的区间一分为二,用w和x两个节点表示。
标记v的左儿子是w,右儿子是x
分别构造以w和以x为根的子树(递归)
v区间的最小值 <- min(w区间的最小值,x区间的最小值)
end function
线段树除了最后一层外,前面每一层的结点都是满的,因此线段树的深度h =ceil(log(2n -1))=O(log n)。
(2) 区间查询
区间查询指用户输入一个区间,获取该区间的有关信息,如区间中最大值,最小值,第N大的值等。
比如前面一个图中所示的树,如果询问区间是[0,2],或者询问的区间是[3,3],不难直接找到对应的节点回答这一问题。但并不是所有的提问都这么容易回答,比如[0,3],就没有哪一个节点记录了这个区间的最小值。当然,解决方法也不难找到:把[0,2]和[3,3]两个区间(它们在整数意义上是相连的两个区间)的最小值“合并”起来,也就是求这两个最小值的最小值,就能求出[0,3]范围的最小值。同理,对于其他询问的区间,也都可以找到若干个相连的区间,合并后可以得到询问的区间。
区间查询的伪代码如下:
// node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点
// Lch 和Rch 分别表示指向左右孩子的指针
void Query(node *p, int a, int b) // 当前考察结点为p,查询区间为(a,b]
{
if (a <= p->Left && p->Right <= b)
// 如果当前结点的区间包含在查询区间内
{
...... // 更新结果
return;
}
Push_Down(p); // 等到下面的修改操作再解释这句
int mid = (p->Left + p->Right) / 2; // 计算左右子结点的分隔点
if (a < mid) Query(p->Lch, a, b); // 和左孩子有交集,考察左子结点
if (b > mid) Query(p->Rch, a, b); // 和右孩子有交集,考察右子结点
}
可见,这样的过程一定选出了尽量少的区间,它们相连后正好涵盖了整个[l,r],没有重复也没有遗漏。同时,考虑到线段树上每层的节点最多会被选取2个,一共选取的节点数也是O(log n)的,因此查询的时间复杂度也是O(log n)。
线段树并不适合所有区间查询情况,它的使用条件是“相邻的区间的信息可以被合并成两个区间的并区间的信息”。即问题是可以被分解解决的。
(3) 区间修改
当用户修改一个区间的值时,如果连同其子孙全部修改,则改动的节点数必定会远远超过O(log n)个。因而,如果要想把区间修改操作也控制在O(log n)的时间内,只修改O(log n)个节点的信息就成为必要。
借鉴前一节区间查询用到的思路:区间修改时如果修改了一个节点所表示的区间,也不用去修改它的儿子节点。然而,对于被修改节点的祖先节点,也必须更新它所记录的值,否则查询操作就肯定会出问题(正如修改单个节点的情况一样)。
这些选出的节点的祖先节点直接更新值即可,而选出的节点的子孙却显然不能这么简单地处理:每个节点的值必须能由两个儿子节点的值得到,如这幅图中的例子:
这里,节点[0,1]的值应该是4,但是两个儿子的值又分别是3和5。如果查询[0,0]区间的RMQ,算出来的结果会是3,而正确答案显然是4。
问题显然在于,尽管修改了一个节点以后,不用修改它的儿子节点,但是它的儿子节点的信息事实上已经被改变了。这就需要我们在节点里增设一个域:标记。把对节点的修改情况储存在标记里面,这样,当我们自上而下地访问某节点时,就能把一路上所遇到的所有标记都考虑进去。
但是,在一个节点带上标记时,会给更新这个节点的值带来一些麻烦。继续上面的例子,如果我把位置0的数字从4改成了3,区间[0,0]的值应该变回3,但实际上,由于区间[0,1]有一个“添加了1”的标记,如果直接把值修改为3,则查询区间[0,0]的时候我们会得到3+1=4这个错误结果。但是,把这个3改成2,虽然正确,却并不直观,更不利于推广(参见下面的一个例子)。
为此我们引入延迟标记的一些概念。每个结点新增加一个标记,记录这个结点是否被进行了某种修改操作(这种修改操作会影响其子结点)。还是像上面的一样,对于任意区间的修改,我们先按照查询的方式将其划分成线段树中的结点,然后修改这些结点的信息,并给这些结点标上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个结点p ,并且决定考虑其子结点,那么我们就要看看结点p 有没有标记,如果有,就要按照标记修改其子结点的信息,并且给子结点都标上相同的标记,同时消掉p 的标记。代码框架为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
void Change(node *p, int a, int b)
{
if (a <= p->Left && p->Right <= b)
{
......
return ;
}
Push_Down(p);
int mid = (p->Left + p->Right) / 2;
if (a < mid) Change(p->Lch, a, b);
if (b > mid) Change(p->Rch, a, b);
Update(p);
|
4.线段树求区间最大值
#include <iostream>
using namespace std;
/* 线段树求区间最大值 */
#define MAXN 100 /* 最大元素个数 */
#define MAX(a,b) ((a) > (b) ? (a) : (b))
struct Node
{
int left;
int right;
int maxV;
}segTree[4*MAXN]; /* 线段树 */
int arr[] = {56,23,10,78,9,
100,26,52,30,18}; /* 测试数组 */
/* 根据测试数组建立线段树 根节点开始构造[left,right]区间最大值*/
void construct(int index, int left, int right)
{
segTree[index].left = left;
segTree[index].right = right;
if(left == right) // 叶子,即为该区间最大值,构造结束
{
segTree[index].maxV = arr[left];
return;
}
int mid = (left + right) >> 1;
construct((index<<1)+1, left, mid);
construct((index<<1)+2, mid+1, right);
segTree[index].maxV = MAX(segTree[(index<<1)+1].maxV,segTree[(index<<1)+2].maxV);
}
/* 线段树从根节点查询某个区间的最大值 */
int query(int index, int left, int right)
{
if(segTree[index].left == left && segTree[index].right == right)
{
return segTree[index].maxV;
}
int mid = (segTree[index].left+segTree[index].right) >> 1;
if(right <= mid) // 注:此区间为[0,mid] 故 <=
{
return query((index<<1)+1,left,right);
}else if(left > mid) // 注:此区间为[mid+1,right] 故 >
{
return query((index<<1)+2,left,right);
}
return MAX(query((index<<1)+1,left,mid), query((index<<1)+2,mid+1,right));
}
void main()
{
construct(0,0,sizeof arr / sizeof(int) - 1);
// 注:construct和query函数省略了边界检查
printf("max of [0,9] is: %d\n",query(0,0,9));
printf("max of [1,4] is: %d\n",query(0,1,4));
printf("max of [3,7] is: %d\n",query(0,3,7));
printf("max of [6,9] is: %d\n",query(0,6,9));
}
5. 线段树求点在线段中的出现次数
#include<iostream>
using namespace std;
/* 线段树求点在已知线段中的出现次数 */
#define BORDER 100 // 设线段端点坐标不超过100
struct Node // 线段树
{
int left;
int right;
int counter;
}segTree[4*BORDER];
/* 构建线段树 根节点开始构建区间[lef,rig]的线段树*/
void construct(int index, int lef, int rig)
{
segTree[index].left = lef;
segTree[index].right = rig;
if(lef == rig) // 叶节点
{
segTree[index].counter = 0;
return;
}
int mid = (lef+rig) >> 1;
construct((index<<1)+1, lef, mid);
construct((index<<1)+2, mid+1, rig);
segTree[index].counter = 0;
}
/* 插入线段[start,end]到线段树, 同时计数区间次数 */
void insert(int index, int start, int end)
{
if(segTree[index].left == start && segTree[index].right == end)
{
++segTree[index].counter;
return;
}
int mid = (segTree[index].left + segTree[index].right) >> 1;
if(end <= mid)
{
insert((index<<1)+1, start, end);
}else if(start > mid)
{
insert((index<<1)+2, start, end);
}else
{
insert((index<<1)+1, start, mid);
insert((index<<1)+2, mid+1, end);
}
}
/* 查询点x的出现次数
* 从根节点开始到[x,x]叶子的这条路径中所有点计数相加方为x出现次数
*/
int query(int index, int x)
{
if(segTree[index].left == segTree[index].right) // 走到叶子,返回
{
return segTree[index].counter;
}
int mid = (segTree[index].left+segTree[index].right) >> 1;
if(x <= mid)
{
return segTree[index].counter + query((index<<1)+1,x);
}
return segTree[index].counter + query((index<<1)+2,x);
}
/* 测试线段 */
int segment[10][2] = {
5, 8, 10, 45, 0, 7,
2, 3, 3, 9, 13, 26,
15, 38, 50, 67, 39, 42,
70, 80
};
/* 测试点 :answer: 1,2,2,3,1,3,2,1,0,0 */
int testPoint[10] = {
1, 2, 4, 6, 9,
15, 13, 44, 48, 90
};
void main()
{
construct(0,0,100); // 构建[0,100]线段树
for(int j = 0; j < 10; ++j) // 插入测试线段
{
insert(0,segment[j][0],segment[j][1]);
}
for(int i = 0; i < 10; ++i) // 查询点出现次数
{
printf("frequent of point %d is: %d\n",
testPoint[i], query(0,testPoint[i]));
}
}
6. 线段覆盖长度
#include<iostream>
using namespace std;
/* 排序求线段覆盖长度 */
#define MAXN 100 // 设线段数不超过100
struct segment
{
int start;
int end;
}segArr[100];
/* 计算线段覆盖长度 */
int lenCount(segment * segArr, int size)
{
int length = 0, start = 0, end = 0;
for(int i = 0; i < size; ++i)
{
start = segArr[i].start;
end = segArr[i].end;
while(end >= segArr[i+1].start)
{
++i;
end = segArr[i].end > end ? segArr[i].end : end;
}
length += (end - start);
}
return length;
}
/* 快排比较函数 (从小到大进行排序)*/
int cmp(const void * p, const void *q)
{
if(((segment *)p)->start != ((segment *)q)->start)
{
return ((segment *)p)->start - ((segment *)q)->start;
}
return ((segment *)p)->end - ((segment *)q)->end;
}
/* 测试线段 answer: 71 */
int segTest[10][2] = {
5, 8, 10, 45, 0, 7,
2, 3, 3, 9, 13, 26,
15, 38, 50, 67, 39, 42,
70, 80
};
void main()
{
for(int i = 0; i < 10; ++i) // 测试线段
{
segArr[i].start = segTest[i][0];
segArr[i].end = segTest[i][1];
}
qsort(segArr,10,sizeof(segment),cmp); // 排序
printf("length: %d\n",lenCount(segArr,10)); // 计算
}