线段树的学习记录
1、区间查询:询问某段区间的某些性质(极值、求和、etc)
2、区间更新:某些操作影响了某些区间(统一加一个值)
3、三个问题: 更新点,查询区间
更新区间,查询点
更新区间,查询区间
例如:一个长度为N的数组a[1]~a[n]:
我们每次对该数组有一些操作:
1、修改数组中某个元素的值
【1,5,4,1,6】---(a[2]=3)---> 【1,3,4,1,6】
2、询问数组中某段区间的最大值
【1,5,4,1,6】---(max(1,4)=?)---> 5
3、询问数组中某段区间的和
【1,5,4,1,6】---(sum(3,5)=?)---> 11
如果只有一次询问?
枚举相应区间内的元素,输出答案 O(N)
更多的询问?
Q次询问,O(NQ) That's too SLOW!
线段树——在O(log2N)的时间内完成每次操作 O(Qlog2N)
Less than 1 second when N=Q=100000
线段树的本质是一棵二叉树,不同于其它二叉树,线段树的每一个节点记录的是一段区间的信息。
对于任一非叶子节点,若该区间为[L,R],则
左儿子为[L,(L+R)/2]
右儿子为[(L+R)/2+1,R]
每一个节点记录的信息?
struct Tree
{
int left,right; //区间的端点
int max,sum; //视题目要求而定
};
线段树——代码实现(建树)
void build(int id,int l,int r){ tree[id].left=l; tree[id].right=r; if (l==r) { tree[id].sum=tree[id*2].sum+tree[id*2+1].sum; tree[id].max=max(tree[id*2].max,tree[id*2+1].max; } else { int mid=(l+r)/2; build(id*2,l,mid); build(id*2+1,mid+1,r); tree[id].sum=tree[id*2].sum+tree[id*2+1].sum; tree[id].max=max(tree[id*2].max,tree[id*2+1].max; } }
线段树——代码实现(更新)
更新某个点的数值,并维护相关点的信息
void update(int id,int pos,int val){ if (tree[id].left==tree[id].right) { tree[id].sum=tree[id].max=val; } else { int mid=(tree[id].left+tree[id].right)/2; if (pos<=mid) update(id*2,pos,val); else update(id*2+1,pos,val); tree[id].sum=tree[id*2].sum+tree[id*2+1].sum; tree[id].max=max(tree[id*2].max,tree[id*2+1].max) } }
查询某区间内元素的和或最大值(以总和为例)
void query(int id,int l,int r){ if (tree[id].left==l&&tree[id].right==r) return tree[id].sum; //询问总和 else { int mid=(tree[id].left+tree[id].right)/2; if (r<=mid) return query(id*2,l,r); else if (l>mid) return query(id*2+1,l,r) else return query(id*2,l,mid)+query(id*2+1,mid+1,r); } }
线段树——空间复杂度
更新操作:由于总是一条路径从根到某个叶子,而树的深度为log2N,因而为O(log2N)
查询操作:每层被访问的节点不超过4个,因而同样为O(log2N)
线段树——空间复杂度
设长度为N的数组在线段树中,编号最靠右的节点编号为F(N)
若N=2n, F(N)=2(n+1)
若N=2(n+1),F(N)=2(n+2)
因而对于2n<=N<=2(n+1),有
2(n+1)<=F(N)<=2(n+2)
F(N)<=4*N
1、线段树可以做很多很多与区间有关的事情……
2、空间复杂度~O(N*4),每次更新和查询操作的复杂度都是O(logN)。
3、在更新和查询区间[l,r]的时候,为了保证复杂度是严格的O(logN)必须在达到被[l,r]覆盖的区间的结点时就立即返回。而为了保证这样做的正确性,需要在这两个过程中做一些相关的“懒”操作。
“懒操作”在更新区间的有关问题上至关重要。