线段树
一.概念(构建)
线段树是一种基于二分思想的完全二叉树,用于维护一段数据或数组,并且使得所有操作的时间复杂度尽量维持在O(nlogn)。
对于未安排数据的节点都选择默认为0,且每一个节点存储的值都是左右孩子进行一些操作的结果(叶节点除外)。
1.代码实现
其实可以直接以数组实现,但为了实现再存储原数组左右区间的值,就使用结构体实现线段树
#include<iostream>
using namespace std;
const int N = 1e4 +5;
int arr[N];
struct segmentTree{
int l,r;
int val;
}tree[4*N];
void build(int p,int l,int r) {
tree[p].l = l;
tree[p].r = r;
//终止条件
if(l==r) {
tree[p].val = arr[l];
}
else {
//本质上是递归的过程
int mid = (l+r)/2;
build(2*p+1,l,mid);
build(2*p+2,mid+1,r);
tree[p].val = tree[2*p+1].val + tree[2*p+2].val;
}
}
int main( ) {
for(int i=0;i<=5;i++) {
cin>>arr[i];
}
build(0,0,5);
for(int i=0;i<15;i++) {
printf("tree[%d,%d]=%d\n",tree[i].l,tree[i].r,tree[i].val);
}
return 0;
}
2.运行结果
tree后面跟着的是区间值,一些未进入的结点,全部都初始化为0
二.基本操作
1.查询(单点 or 区间)
查询区间和线段树节点区间相交的不同情况:
- 查询区间完全包含线段树节点区间------------直接返回该节点tree[p].sum
- 查询区间和线段树节点左孩子有交集---------往下递归query(2*p+1,L,R)
- 查询区间和线段树节点右孩子有交集---------往下递归query(2*p+2,L,R)
单点查询就是区间查询的特殊情况,此时L = R,对以下代码稍微修改一下就可以了
int query(int p,int L,int R) {
int sum = 0;
//终止条件
if(tree[p].l>R||tree[p].r<L)
sum = 0;//不在范围内直接返回
else if(tree[p].l>=L&&R>=tree[p].r)
sum += tree[p].val;//包含其中直接返回这个节点的值
else {
//以上两种情况都不包含则是有交集的情况,对左右孩子都递归,不符合会直接返回
int mid = (L+R)/2;
sum += query(2*p+1,L,R);
sum += query(2*p+2,L,R);
}
return sum;
}
测试实例
2.单点修改
对单点进行修改就是对于单点查询做一点修改,在查询到修改节点值之后,再更新每个祖先节点的值。
//单点修改
void update(int p,int ind,int val) {
if(ind < tree[p].l||ind > tree[p].r )
return;
else if(tree[p].r == tree[p].l) {
tree[p].val = val;
}
else {
update(2*p+1,ind,val);
update(2*p+2,ind,val);
//这一步很重要,一定不能少
tree[p].val = tree[2*p+1].val + tree[2*p+2].val;
}
}
3.区间修改(带pushdown())
区间修改就是某一个区间的值都要进行修改,可能是将某一个区间的所有值都加一操作,如果把每一个参与进去的祖先和子孙节点都进行修改,会超时,这个时候线可以只改动祖先而子节点在要用到的时候再进行修改。用一个lazy标记(延迟标记),在要用到的时候再使用标记进行操作。
//区间修改,此处是将[L,R]所有元素全部加一
void change(int p,int L,int R) {
if(R < tree[p].l||L > tree[p].r )
return;
if(L <= tree[p].l && tree[p].r <= R) {
//完全包含其中,直接改变标记并处理然后返回即可
tree[p].val += (tree[p].r - tree[p].l +1);
tree[p].lazy += 1;
return;
}
//处理延迟标记,因为要用到左右儿子节点了
pushdown(p);
int mid = (tree[p].l + tree[p].r)/2;
if(L <= mid) change(2*p+1,L,R);
if(R > mid) change(2*p+2,L,R);
tree[p].val = tree[2*p+1].val + tree[2*p+2].val;
}
接下来介绍pushdown()函数,这个函数的作用就是处理延迟标记,对于被标记了的节点,即lazy不为0,则先把标记传递给左右儿子,然后处理左右儿子的内容,最后再将这个标记清零,因为已经处理完成。
void pushdown(int p) {
if(tree[p].lazy != 0){
tree[2*p+1].lazy += tree[p].lazy;
tree[2*p+2].lazy += tree[p].lazy;
int mid = (tree[p].l + tree[p].r)/2;
tree[2*p+1].val += mid - tree[p].l + 1;
tree[2*p+2].val += tree[p].r - mid;
tree[p].lazy = 0;//父节点的标记清零
}
}
最后提一点因为区间修改用的是懒标记,这个时候的查询已经不是上面那个查询了,同样是需要用到pushdown()函数的查询函数,但函数大体并没有发生改变。
//查询
int _query(int p,int L,int R) {
//终止条件
if(tree[p].l>R||tree[p].r<L)
return 0;
int sum = 0;
pushdown(p);
if(tree[p].l>=L&&R>=tree[p].r) {
sum += tree[p].val;
}
else {
int mid = (L+R)/2;
sum += _query(2*p+1,L,R);
sum += _query(2*p+2,L,R);
}
return sum;
}
三.特殊的线段树
还有两个比较特特殊的乘法线段树和根号线段树,等以后再补吧。。。