在ACM竞赛中,线段树是一种特殊的数据结构,总的来说它支持两种操作,一是更新,二是查询。当然,不使用线段树也能完成这两种操作,此时一般为线性复杂度O(n),所以使用它的目的其实也就是优化时间复杂度,更新和查询操作的复杂度都下降到了o(logn)。
其次,说说线段树的实现思想。总体上看,线段树将整个区间不断二分,直到区间上下界重合,最终形成一棵树,然后给每一个子区间从上到下、从左到右标上号,从这一步看来,线段树和一般的二叉树十分相似,唯一的区别就是线段树中的每一个节点保存的是一段区间的信息,所以整个线段树便可以用一个数组保存。接着就是最重要的一步,为每一个标上号的节点设计一个信息保存结构,用于存储和更新我们感兴趣的相关信息,比如和、极值等。最后,就是如何动态的去维护这些信息了。比如对于数组a[1,5,4,1,6],它有5个元素,由其建立的线段树及其维护的区间信息如下如下:
下面说说线段树解决的一般问题,针对于更新和查找操作,无非可以分为以下几类:1、点更新,查询区间;2、区间更新,查询点;3、区间更新,查询区间。
最后就需要具体编程来实现线段树这一数据结构了。虽然看起来线段树的用法可以有很多变形,比如其维护的信息就可以有多种选择,但是毕竟线段树支持的功能就只有更新和查询,所以一般的编写流程还是比较固定的。
1、设计线段树节点的数据结构,保存需要维护的区间信息
线段树的每一个节点都保存了一段区间信息,这一步就需要明确哪一些信息是自己感兴趣的,当然可能需要维护的信息不止一种,所以一般使用结构体来实现。还有一个问题就是对于一个有n个元素的数组,其线段树的节点有多少个呐,由于线段树将n不断分成两半,所以仔细分析一下可知,线段树大概不会超过4*n个节点。
const int MAXN = 1e5 + 5;
struct SegTree
{
int l, r; // 当前节点对应的区间
LL lazy; // 用于区间更新时使用
LL sum, maxs, mins; // 需要维护的区间信息
inline int len() {return r-l+1;}
}tree[4*MAXN];
2、接下来考虑建立线段树接的过程
建立线段树的方法,一般有三种:1、采用点更新的方式,每读入一个数据就更新一个;2、以范围为依据一直递归到叶子节点,过程中不断建立节点;3、另外一种自底向上的写法,速度最快。这里使用第二种,一般来说效率也不错了。这一步还需要理解两个步骤,一个就是递归,另一个就是线段树左右节点的确定。递归建树的过程容易理解,父节点、左右节点之间关系的确定的话,按照前面的标号规则,不难发现第i个节点的左孩子是第2*i个节点,右孩子是第2*i+1个节点。这里仅以维护区间和信息为例,其它信息的维护类似。
// 向上更新
void PushUp(int id)
{
// 更新区间和信息
tree[id].sum = tree[lid].sum + tree[rid].sum;
// 也可以更新其它信息
// ......
}
// 递归建树,调用build(1,1,n)即可
void build(int id, int bl, int br)
{
// 到达叶子节点,输入初始化信息
if(bl == br)
{
// 维护区间和信息
scanf("%lld", &tree[id].sum);
// 也可以维护其它信息
// ......
}
else
{
int mid = (bl + br) >> 1;
// 递归建立左子树
build(lid, bl, mid);
// 递归建立右子树
build(rid, mid+1, br);
// 将信息更新给上层节点
PushUp(id);
}
// 维护区间范围信息
tree[id].l = bl;
tree[id].r = br;
tree[id].lazy = 0;
}
3、实现更新节点信息操作
更新操作比较复杂,也比较繁琐,按线段树的设计功能来说,其大致可以分为下面几类:
1) 单点更新,这一类更新操作比较简单,几乎不用其它特殊的辅助标记就能完成
// 单点更新操作,将a[pos]的值改为v,调用update1(1,pos,v)即可
void update1(int id, int pos, LL v)
{
int l = tree[id].l;
int r = tree[id].r;
// 到达叶子节点,更新信息
if(l == r)
{
// 进行修改值操作
tree[id].sum = v;
// 也可进行其它更新操作
// ......
}
else
{
// 判断需要更新的节点所处区间
int m = (l+r) >> 1;
// 向左子树走
if(pos <= m) update1(lid, pos, v);
// 向右子树走
else update1(rid, pos, v);
// 将信息更新给上层节点
PushUp(id);
}
}
2) 区间更新,这一种操作比较复杂,不能使用线段树的原始定义直接进行更新操作,否则如果给出的更新区间接近于原始数组的长度,线段树的更新复杂度将退化到大于O(n),因为每一个叶子节点都被更新了。为了保持严格O(logn)的时间复杂度,这一步操作将引入一个特殊的标记,称为懒标记,也叫延迟标记,它的意思就是说当更新区间的时候,当满足当前节点的区间完全包含被更新区间的时候,就在当前节点上进行一个懒操作记录必要的信息,然后更新操作就不再继续向下进行了,即用父节点代为保存子节点的信息,借此已达到优化时间的目的。那如何保证下层节点维护信息的正确性呐,其实只要在当需要递归处理下层节点的时候,此时将当前节点的标记分解,传递给两个子节点进行计算即可。
// 向下更新
void PushDown(int id)
{
if(!tree[id].lazy) return ;
// 更新懒标记、区间和信息
tree[lid].lazy += tree[id].lazy;
tree[rid].lazy += tree[id].lazy;
tree[lid].sum += tree[id].lazy * tree[lid].len();
tree[rid].sum += tree[id].lazy * tree[rid].len();
tree[id].lazy = 0;
// 也可以更新其它信息
// ......
}
// 区间更新操作,将a[ul]-a[ur]的值增加v,调用update2(1,ul,ur,v)即可
void update2(int id, int ul, int ur, LL v)
{
int l = tree[id].l;
int r = tree[id].r;
// 到达完全包含更新区间的节点,更新信息
if(l==ul && r==ur)
{
// 进行增加值操作
tree[id].lazy += v;
tree[id].sum += v * tree[id].len();
// 也可进行其它更新操作
// ......
}
else
{
// 向下更新信息
PushDown(id);
int mid = (l + r) >> 1;
// 向左子树走
if(ur <= mid) update2(lid, ul, ur, v);
// 向右子树走
else if(ul > mid) update2(rid, ul, ur, v);
// 向左、右子树走
else
{
update2(lid, ul, mid, v);
update2(rid, mid+1, ur, v);
}
// 向上更新信息
PushUp(id);
}
}
4、最后实现查询操作
查询操作比较简单,只需要递归找到对应区间的节点即可。但是需要注意的是,如果同时还进行的是区间更新操作,那么此时查询操作也需要对懒标记进行处理。
// 查询操作,返回a[ql]-a[qr]的和,调用query(1,ql,qr)即可
LL query(int id, int ql, int qr)
{
int l = tree[id].l;
int r = tree[id].r;
// 当前节点的区间完全包含在查询区间中,则返回节点区间和信息
if(l==ql && r==qr)
{
// 也可以返回其它信息
// ......
return tree[id].sum;
}
// 向下更新数据,有懒标记时有用
PushDown(id);
int mid = (l + r) >> 1;
// 向左子树走
if(qr <= mid) return query(lid, ql, qr);
// 向右子树走
if(ql > mid) return query(rid, ql, qr);
// 向左、右子树走
return query(lid, ql, mid) + query(rid, mid+1, qr);
}
以一道例题为例,演示线段树的用法,POJ:3468,时空转移(点击打开链接),题目如下:
Time Limit: 5000MS | Memory Limit: 131072K | |
Total Submissions: 73751 | Accepted: 22726 | |
Case Time Limit: 2000MS |
Description
You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.
Input
The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1, A2, ... , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
"C a b c" means adding c to each of Aa, Aa+1, ... , Ab. -10000 ≤ c ≤ 10000.
"Q a b" means querying the sum of Aa, Aa+1, ... , Ab.
Output
You need to answer all Q commands in order. One answer in a line.
Sample Input
10 5 1 2 3 4 5 6 7 8 9 10 Q 4 4 Q 1 10 Q 2 4 C 3 6 3 Q 2 4
Sample Output
4 55 9 15
Hint
Source
题意:
给出一个数组,对其有两种操作,一是将某范围内的元素都加上一个值,二是查询某范围内的元素和。
分析:
数组长度,以及操作次数都比较大,所以这是标准的线段树区间更新、查询区间和。
源代码:
#include <cstdio>
#include <cstring>
#define lid (id << 1)
#define rid (id << 1 | 1)
#define LL long long
const int MAXN = 1e5 + 5;
struct SegTree
{
int l, r; // 当前节点对应的区间
LL lazy; // 用于区间更新时使用
LL sum, maxs, mins; // 需要维护的区间信息
inline int len() {return r-l+1;}
}tree[4*MAXN];
// 向上更新
void PushUp(int id)
{
// 更新区间和信息
tree[id].sum = tree[lid].sum + tree[rid].sum;
// 也可以更新其它信息
// ......
}
// 向下更新
void PushDown(int id)
{
if(!tree[id].lazy) return ;
// 更新懒标记、区间和信息
tree[lid].lazy += tree[id].lazy;
tree[rid].lazy += tree[id].lazy;
tree[lid].sum += tree[id].lazy * tree[lid].len();
tree[rid].sum += tree[id].lazy * tree[rid].len();
tree[id].lazy = 0;
// 也可以更新其它信息
// ......
}
// 递归建树,调用build(1,1,n)即可
void build(int id, int bl, int br)
{
// 到达叶子节点,输入初始化信息
if(bl == br)
{
// 维护区间和信息
scanf("%lld", &tree[id].sum);
// 也可以维护其它信息
// ......
}
else
{
int mid = (bl + br) >> 1;
// 递归建立左子树
build(lid, bl, mid);
// 递归建立右子树
build(rid, mid+1, br);
// 将信息更新给上层节点
PushUp(id);
}
// 维护区间范围信息
tree[id].l = bl;
tree[id].r = br;
tree[id].lazy = 0;
}
// 单点更新操作,将a[pos]的值增加v,调用update1(1,pos,v)即可
void update1(int id, int pos, LL v)
{
int l = tree[id].l;
int r = tree[id].r;
// 到达叶子节点,更新信息
if(l == r)
{
// 进行增加值操作
tree[id].sum += v;
// 也可进行其它更新操作
// ......
}
else
{
// 判断需要更新的节点所处区间
int m = (l+r) >> 1;
// 向左子树走
if(pos <= m) update1(lid, pos, v);
// 向右子树走
else update1(rid, pos, v);
// 将信息更新给上层节点
PushUp(id);
}
}
// 区间更新操作,将a[ul]-a[ur]的值增加v,调用update2(1,ul,ur,v)即可
void update2(int id, int ul, int ur, LL v)
{
int l = tree[id].l;
int r = tree[id].r;
// 到达完全包含更新区间的节点,更新信息
if(l==ul && r==ur)
{
// 进行增加值操作
tree[id].lazy += v;
tree[id].sum += v * tree[id].len();
// 也可进行其它更新操作
// ......
}
else
{
// 向下更新信息,下放懒标记
PushDown(id);
int mid = (l + r) >> 1;
// 向左子树走
if(ur <= mid) update2(lid, ul, ur, v);
// 向右子树走
else if(ul > mid) update2(rid, ul, ur, v);
// 向左、右子树走
else
{
update2(lid, ul, mid, v);
update2(rid, mid+1, ur, v);
}
// 向上更新信息
PushUp(id);
}
}
// 查询操作,返回a[ql]-a[qr]的和,调用query(1,ql,qr)即可
LL query(int id, int ql, int qr)
{
int l = tree[id].l;
int r = tree[id].r;
// 当前节点的区间完全包含在查询区间中,则返回节点区间和信息
if(l==ql && r==qr)
{
// 也可以返回其它信息
// ......
return tree[id].sum;
}
// 向下更新数据,有懒标记时有用
PushDown(id);
int mid = (l + r) >> 1;
// 向左子树走
if(qr <= mid) return query(lid, ql, qr);
// 向右子树走
if(ql > mid) return query(rid, ql, qr);
// 向左、右子树走
return query(lid, ql, mid) + query(rid, mid+1, qr);
}
int main()
{//freopen("sample.txt", "r", stdin);
LL n, m;
while(~scanf("%lld%lld", &n, &m))
{
build(1, 1, n);
while(m--)
{
char op[2];
int a, b, c;
scanf("%s", op);
if(op[0] == 'Q')
{
scanf("%d%d", &a, &b);
printf("%lld\n", query(1, a, b));
}
else
{
scanf("%d%d%d", &a, &b, &c);
update2(1, a, b, c);
}
}
}
return 0;
}
这里讨论的线段树是一维情况下的,但是i和树状数组一样,线段树也可以扩展到二维,详细信息可以去这里了解()。