一:为什么需要线段树?
题目1:
10000个正整数,编号从1到10000,用A[1],A[2],A[10000]表示。
修改:1.将第L个数增加C (1 <= L <= 10000)
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.
上面的问题就是典型的线段树点修改。
线段树先将区间[1..10000]分成不超过4*10000个子区间,对于每个子区间,记录一段连续数字的和。
之后,任意给定区间[L,R],线段树在上述子区间中选择约2*log2(R-L+1)个拼成区间[L,R]。
如果A[L]+=C ,线段树的子区间中,约有log2(10000)个包含了L,所以需要修改log2(10000)个。
于是,使用线段树的话,
A[L]+=C 需要修改log2(10000) 个元素
求和A[L...R]需要修改2*log2(R-L+1) <= 2 * log2(10000) 个元素。
log2(10000) < 14 所以相对来说线段树的修改和求和都比较快。
问题一:开始的子区间是怎么分的?
首先是讲原始子区间的分解,假定给定区间[L,R],只要L < R ,线段树就会把它继续分裂成两个区间。
首先计算 M = (L+R)/2,左子区间为[L,M],右子区间为[M+1,R],然后如果子区间不满足条件就递归分解。
以区间[1..13]的分解为例,分解结果见下图:
问题二:给定区间【L,R】,如何分解成上述给定的区间?
对于给定区间[2,12]要如何分解成上述区间呢?
分解方法一:自下而上合并——利于理解
先考虑树的最下层,将所有在区间[2,12]内的点选中,然后,若相邻的点的直接父节点是同一个,那么就用这个父节点代替这两个节点(父节点在上一层)。这样操作之后,本层最多剩下两个节点。若最左侧被选中的节点是它父节点的右子树,那么这个节点会被剩下。若最右侧被选中的节点是它的父节点的左子树,那么这个节点会被剩下。中间的所有节点都被父节点取代。对最下层处理完之后,考虑它的上一层,继续进行同样的处理。
下图为n=13的线段树,区间[2,12],按照上面的叙述进行操作的过程图:
由图可以看出:在n=13的线段树中,[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12] 。
分解方法二:自上而下分解——利于计算
首先对于区间[1,13],计算(1+13)/2 = 7,于是将区间[2,12]“切割”成了[2,7]和[8,12]。
其中[2,7]处于节点[1,7]的位置,[2,7] < [1,7] 所以继续分解,计算(1+7)/2 = 4, 于是将[2,7] 切割成[2,4]和[5,7]。
[5,7]处于节点[5,7]的位置,所以不用继续分解,[2,4]处于区间[1,4]的位置,所以继续分解成[2]和[3,4]。
最后【2】 < 【1,2】,所以计算(1+2)/2=1 ,将【2】用1切割,左侧为空,右侧为【2】
当然程序是递归计算的,不是一层一层计算的,上图只表示计算方法,不代表计算顺序。
问题三:如何进行区间统计?
假设这13个数为1,2,3,4,1,2,3,4,1,2,3,4,1. 在区间之后标上该区间的数字之和:
如果要计算[2,12]的和,按照之前的算法:
[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12]
29 = 2 + 7 + 6 + 7 + 7
计算5个数的和就可以算出[2,12]的值。
问题四:如何进行点修改?
假设把A[6]+=7 ,看看哪些区间需要修改?[6],[5,6],[5,7],[1,7],[1,13]这些区间全部都需要+7.其余所有区间都不用动。
于是,这颗线段树中,点修改最多修改5个线段树元素(每层一个)。
下图中,修改后的元素用蓝色表示。
问题五:存储结构是怎样的?
线段树是一种二叉树,当然可以像一般的树那样写成结构体,指针什么的。
但是它的优点是,它也可以用数组来实现树形结构,可以大大简化代码。
数组形式适合在编程竞赛中使用,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。
简单的记法: 足够的空间 = 数组大小n的四倍。
实际上足够的空间 = (n向上扩充到最近的2的某个次方)的两倍。
举例子:假设数组长度为5,就需要5先扩充成8,8*2=16.线段树需要16个元素。如果数组元素为8,那么也需要16个元素。
所以线段树需要的空间是n的两倍到四倍之间的某个数,一般就开4*n的空间就好,如果空间不够,可以自己算好最大值来省点空间。
怎么用数组来表示一颗二叉树呢?假设某个节点的编号为v,那么它的左子节点编号为2*v,右子节点编号为2*v+1。
然后规定根节点为1.这样一颗二叉树就构造完成了。通常2*v在代码中写成 v<<1 。 2*v+1写成 v<<1|1 。
代码如何实现呢:
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 5;
int sum[maxn], a[maxn], n;
void pushup(int x)
{
sum[x] = sum[x * 2] + sum[x * 2 + 1];
}
void build(int l , int r, int x) //建树 [l,r]表示当前节点区间, x表示当前节点的储存下标(其实是二分图的特性);
{
if(l == r){ //到达最后的叶子节点
sum[x] = a[l];
return ;
}
int mid = (l + r) / 2;
build(l , mid, x * 2); //左儿子建树
build(mid + 1, r, x * 2 + 1); //右儿子建树
pushup(x); //下推更新
}
void update(int L,int c,int l, int r, int x) //单点修改 [l,r]表示当前节点区间, x表示当前节点的储存下标,L为修改点下标, c为a[L] += c;
{
if(l == r) {
sum[x] += c; //找到最后的叶子节点
return ;
}
int mid = (l + r) / 2;
if(L <= m) update(L, c, l, mid, x * 2); // 调用左儿子
else update(L, c, mid + 1, r, x * 2 + 1); //调用右儿子
pushup(x);
}
int query(int L, int R,int l, int r, int x) //区间查询 [l,r]表示当前节点区间, x表示当前节点的储存下标 , [L, R]为你想要查询的区间
{
if(L <= l && r <= R){
return sum[x]; //[l, r] 完整的存在于[L,R] 是直接返回值;
}
int mid = (l + r) / 2;
int ans = 0;
if(L <= m) ans += query(L, R, l, mid, x * 2); //调用左儿子
if(R > m) ans += query(L, R, mid + 1, r,x * 2 + 1); //调用右儿子
return ans;
}
区间成段更新
Lazy:正常来说,区间改值,当更改某个区间的值的时候,子区间也该跟着更改,这样容易TLE。
Lazy思想就是更新到某个区间的时候,就先给这个区间打上标记,标记内容是需要更新的值,并把子区间的值改为子区间对应的值,清除该区间的lazy标记;然后return,不去更新子区间。当下一次更新或查询等需要访问该区间的子区间的时候再把该区间的lazy和其他信息送回子区间。
举个简单粗暴的例子:
对应下面的那个图,假如目的是求和,现在要给[1,6] 的值都加2,那么我们从[1,12]->[1,6],然后[1,6]的sum值加上区间长度[ (6-1+1)*2 ],再把[1,6]的add[i]设置为2,就不再往下更新了【这里极大提高效率】。下一次更新/查询[1,6]的子区间时,我们将[1,6]原存的add值下传给[1,6]的两个直接子区间,再往下更新。假设在这种情况下,我们再更新[1,6]加3,则[1,6]的add值为2+3=5,然后我们查询[1,3],则从上往下经过[1,6]时把[1,6]的add值给了子区间[1,3]和[4,6],同时把sum[子区间]跟着子区间长度和add[父结点]改动,清除add[父节点]。【如果是查询间接子区间,则连续传递add值,也就是连续pushDown】
详细例子:假设update()是区间改值,query()是求和,所有叶子区间的和都为1,则[7,8]和[7,9]在build()的时候就附上了值(图中绿色字体)。假设此时我们更新[7,9]的值,改为2,则线段树从[1,12]->[7,12]->[7,9],然后把[7,9]打上值为2的标记,求和(求和直接用区间长度*此时更新的值),然后不去更新[7,8]和[9,9]了,他们值仍然是2和1,lazy值为0。
然后我们查询[7,8],当遍历经过[7,9]时
if(add[i])
pushDown(i);
成立,把[7,9]的lazy标记2传给子区间[7,8]和[9,9],分别求这2个子区间的和,把[7,9]的lazy标记去掉,然后继续遍历,到[7,8]的时候直接返回答案。
————————————————————————————————————————————————————————————
在此通俗的解释我理解的Lazy意思,比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,它的节点标记为rt,这时tree[rt].l == a && tree[rt].r == b 这时我们可以一步更新此时rt节点的sum[rt]的值,sum[rt] += c * (tree[rt].r - tree[rt].l + 1),注意关键的时刻来了,如果此时按照常规的线段树的update操作,这时候还应该更新rt子节点的sum[]值,而Lazy思想恰恰是暂时不更新rt子节点的sum[]值,到此就return,直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间 。
下面通过具体的代码来说明之。
在此先介绍下代码中的函数说明:
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
宏定义左儿子lson和右儿子rson,貌似用宏的速度要慢。
PushUp(rt):通过当前节点rt把值递归向上更新到根节点
PushDown(rt):通过当前节点rt递归向下去更新rt子节点的值
rt表示当前子树的根(root),也就是当前所在的结点
LL sum[N<<2],add[N<<2];
struct Node
{
int l,r;
int mid()
{
return (l+r)>>1;
}
} tree[N<<2];
这里定义数据结构sum用来存储每个节点的子节点数值的总和,add用来记录该节点的每个数值应该加多少
tree[].l tree[].r分别表示某个节点的左右区间,这里的区间是闭区间
下面直接来介绍update函数,Lazy操作主要就是用在这里
void update(int c,int l,int r,int rt)//表示对区间[l,r]内的每个数均加c,rt是根节点
{
if(tree[rt].l == l && r == tree[rt].r)
{
add[rt] += c;
sum[rt] += (LL)c * (r-l+1);
return;
}
if(tree[rt].l == tree[rt].r) return;
PushDown(rt,tree[rt].r - tree[rt].l + 1);
int m = tree[rt].mid();
if(r <= m) update(c,l,r,rt<<1);
else if(l > m) update(c,l,r,rt<<1|1);
else
{
update(c,l,m,rt<<1);
update(c,m+1,r,rt<<1|1);
}
PushUp(rt);
}
if(tree[rt].l == l && r == tree[rt].r) 这里就是用到Lazy思想的关键时刻 正如上面说提到的,这里首先更新该节点的sum[rt]值,然后更新该节点具体每个数值应该加多少即add[rt]的值,注意此时整个函数就运行完了,直接return,而不是还继续向子节点继续更新,这里就是Lazy思想,暂时不更新子节点的值。
那么什么时候需要更新子节点的值呢?答案是在某部分update操作的时候需要用到那部分没有更新的节点的值的时候,这里可能有点绕口。这时就掉用PushDown()函数更新子节点的数值。
void PushDown(int rt,int m)
{
if(add[rt])
{
add[rt<<1] += add[rt];
add[rt<<1|1] += add[rt];
sum[rt<<1] += add[rt] * (m - (m>>1));
sum[rt<<1|1] += add[rt] * (m>>1);
add[rt] = 0;//更新后需要还原
}
}
PushDown就是从当前根节点rt向下更新每个子节点的值,这段代码读者可以自己好好理解,这也是Lazy的关键。
接着就是update操作的三个if语句了
下面再解释query函数,也就是用这个函数来求区间和
__int64 query(int l,int r,int rt)
{
if(l == tree[rt].l && r == tree[rt].r)
{
return sum[rt];
}
PushDown(rt,tree[rt].r - tree[rt].l + 1);
int m = tree[rt].mid();
LL res = 0;
if(r <= m) res += query(l,r,rt<<1);
else if(l > m) res += query(l,r,rt<<1|1);
else
{
res += query(l,m,rt<<1);
res += query(m+1,r,rt<<1|1);
}
return res;
}
第一个if还是区间的判断和前面update的一样,到这里就可以知道答案了,所以就直接return。
接下来的查询就需要用到rt子节点的值了,由于我们用了Lazy操作,这段的数值还没有更新,因此我们需要调用PushDown函数去更新之,满足if(add[rt])就说明还没有更新。
下面给出完整的代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
————————————————————————————————————————————————————————————
下面给出例题,可以练练手:
POJ上3468 就是一道裸的线段树的板子题:
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
The sums may exceed the range of 32-bit integers.
ac代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
还有另一种AC代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include