线段树——入门篇
简介
对于广大的OIer来说,线段树并不是一个陌生的词。作为一种比较高大上的数据结构,线段树有许多的优点。它有着过硬的区间处理功能,在解决区间问题上能够占据一席之地,其区间修改和查询的时间复杂度都是O(log2n)的,因此许多区间性质的问题都可以用它来维护以获得良好的效率,而它本身也可以处理许多基础的区间问题。
当然,它也有缺点,最明显的就是它的空间浪费问题,如果是堆式建法(每次把区间划分成尽量等长的两个区间,递归建出一颗完全二叉树)的话,如果恰好建出一颗满二叉树,则正好开到区间原长的两倍,空间复杂是O(2n)。但如果不是这样的话,则需要开到原长的4倍,这样的话是O(4n),但实际用到的只有一半,这样就造成了大量的空间浪费。不过大部分的题目都是允许开到4倍的,如果遇到少数卡空间的题的话,就需要用到动态开点了。
接下来言归正传,开始学习。如图这就是一颗表示区间[1,n]的线段树(摘自度娘),区间中每一部分的信息都从叶子节点一层层的传上来,所以它能够处理各个区间中的情况。每一次修改或者是查询都从根节点开始,会出现三种情况:
(1)需要更改或是查询的区间完全包括了当前的这个节点所表示的区间。此时这个节点所代表的区间将全部作出更改(查询的话返回这个节点的信息)。
(2)需要更改或是查询的区间与当前的这个节点所表示的区间的左半部分有交集。此时就去查找这个节点的左儿子。
(3)需要更改或是查询的区间与当前的这个节点所表示的区间的右半部分有交集。此时就去查找这个节点的右儿子。
(2)、(3)种情况会交替出现,不断查找直到出现(1)情况,然后更改(或返回信息),再一层层的传递上来。这就是线段树的大体工作形式,个人认为理解这些对接下来的学习很有必要。
最基础的线段树能做什么
裸的线段树一般可以解决以下几种操作:
1>建树
在做其他工作之前先建树(有些题目可以不必建树)。
void build(int rt,int l,int r) { if (l==r) { scanf("%d",&tree[rt]);//读入放在了建树中,也可以在外面读入 return; } int m=(l+r)>>1; build(lchild); build(rchild);//递归建立左右儿子 push_up(rt);//维护 }
2>修改:
单点修改:
修改一个点的值:找到它—>修改—>依次更新上来。
void update(int rt,int l,int r,int p,int k) { if (l==r)//找到了需要更新的点 { tree[rt]+=k;//修改 return; } int m=(l+r)>>1; if (p<=m) update(lchild,p,k); else update(rchild,p,k); push_up(rt);//更新 }区间修改:
修改一个区间的值如果再像修改单点一样效率就有些低了,于是需要用到lazy标记(延迟标记)。每次将需要修改的区间打上lazy标记,然后等到下次修改或查询的时候再下放标记,只需要更新用到的点。
void update(int rt,int l,int r,int ll,int rr,int v) { if (ll<=l && r<=rr)//情况(1) { tree[rt]+=v*(r-l+1); lazy[rt]+=v; return; } if (lazy[rt]) push_down(rt,r-l+1);//如果有lazy标记,先下放标记 int m=(l+r)>>1,k=0; if (ll<=m) update(lchild,ll,rr,v);//情况(2) if (rr>m) update(rchild,ll,rr,v);//情况(3) push_up(rt);//更新 }3>维护:
区间最值:
如果需要的是区间最值(最大或是最小),那么当前节点的值应在左右儿子的最值中(左右儿子最值中的最值)。下面以最大值为例:
void push_up(int rt) { tree[rt]=max(tree[rt<<1],tree[rt<<1|1]);//"<<"是位运算,是在二进制中左移两位,"<<1" 即乘二,"<<1|1"即乘二加一 }区间求和:
如果需要的是区间求和,那么当前节点的值是左右儿子的和的和。(...语言有点不通顺,见谅...)
void push_up(int rt) { tree[rt]=tree[rt<<1]+tree[rt<<1|1]; }4>查询
查询操作一般都是区间查询,单点查询可以看成是左右端点相同的区间。下面以查询区间最大值为例:
int query(int rt,int l,int r,int ll,int rr) { if (ll<=l && r<=rr) return tree[rt];//情况(1) int m=(l+r)>>1,k=0; if (ll<=m) k=max(k,query(lchild,ll,rr));//情况(2) if (rr>m) k=max(k,query(rchild,ll,rr));//情况(3) return k; }以上便是最基础的线段树操作了,掌握了这些就可以做最基本的线段树练习了。最后给出一点小的建议: 最好不要背模板,能够在理解的基础上建立自己的线段树风格是最好的!
练习1.codevs1080线段树练习点击打开链接(单点修改+区间最大值)
练习2.codevs1081线段树练习2点击打开链接(区间修改+单点查询)
练习3.codevs1082线段树练习3点击打开链接(区间修改+区间求和)
练习4.codevs1191数轴染色点击打开链接(区间修改)
练习1.2.3都是裸的线段树操作,4则需要略作思考,不过也是十分基础的线段树,以下为题解:
codevs1191数轴染色
在一条数轴上有N个点,分别是1~N。一开始所有的点都被染成黑色。接着
我们进行M次操作,第i次操作将[Li,Ri]这些点染成白色。请输出每个操作执行后
剩余黑色点的个数。
输入一行为N和M。下面M行每行两个数Li、Ri
输出M行,为每次操作后剩余黑色点的个数。
10 3
3 3
5 7
2 8
9
6
3
数据限制
对30%的数据有1<=N<=2000,1<=M<=2000
对100%数据有1<=Li<=Ri<=N<=200000,1<=M<=200000
可以开始在建树的时候把每个点都置为1,然后当成区间求和来维护,因为只有两种颜色,所以虽然是区间修改,但却不需要用lazy标记。每次更新完后,输出根节点处的值即为剩余的黑点数。
Code:
#include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> #include<algorithm> #define lchild rt<<1,l,m #define rchild rt<<1|1,m+1,r using namespace std; int tree[800001]={0},lazy[800001]={0}; void push_up(int rt) { tree[rt]=tree[rt<<1]+tree[rt<<1|1]; } void build(int rt,int l,int r) { if (l==r) { tree[rt]=1; return; } int m=(l+r)>>1; build(lchild); build(rchild); push_up(rt); } void update(int rt,int l,int r,int ll,int rr) { if (tree[rt]==0) return; if (ll<=l && r<=rr) { tree[rt]=0; return; } int m=(l+r)>>1; if (ll<=m) update(lchild,ll,rr); if (rr>m) update(rchild,ll,rr); push_up(rt); } int main() { int n,m; scanf("%d%d",&n,&m); build(1,1,n); while (m--) { int x,y; scanf("%d%d",&x,&y); update(1,1,n,x,y); printf("%d\n",tree[1]); } return 0; }