区间查询:
询问某些区间的某些性质(极值、求和等);
区间更新:
某些操作影响了某段区间(统一加一个数等);
三个问题:
更新点,查询区间;
更新区间,查询点;
更新区间,查询区间;
时间空间复杂度O(log2N);
线段树的本质是二叉树,不同于其他的二叉树,线段树的每个节点存储的是一段区间、记录的是这个区间的信息。
对于长度为5的数组a[1]~a[5]
[1 5]
[1 3] [4 5]
[1 2] [3 3] [4 4] [5 5]
[1 1] [2 2]
对于任意的非叶子节点,若该区间为[L,R],则左二子为[L,(L+R)/2],右儿子为[(L+R)/2+1,R];
1 5 4 1 6(sum=17,max=6)
1 5 4(max=5,sum=10) 1 6(max=6,sum=7)
1 5(max=5,sum=6) 4(max=4,sum4) 1(max=1,sum=1) 6(max=6,sum7)
1(max=1,sum=1) 5(max=5,sum=5)
修改a[2]=3,修改之后,再做一次pushup,pushuup是自下而上的操作,修改是自上而下,在由下跟新到上;
[1 5](max=6,sum=15)
(max=4,sum=8)[1 3] [4 5](max=6,sum=7)
(max=3,sum4)[1 2] [3 3](max=4,sum=4) (max=1,sum=1)[4 4] [5 5](max=6, sum=6) ( max=1,sum=1)[1 1] [2 2] (max=3,sum=3)
查询[3 5],就是查询区间内完全包括的叶子节点。
每个节点记录的信息
struct Tree
{
int left,right;//区间端点
int sum,maxx;//视题目而定
};
如何记录左右儿子?
[1 5] 1
[1 3] 2 [4 5] 3
[1 2] 4 [3 3] 5 [4 4] 6 [5 5] 7
[1 1] 8 [2 2] 9
我们用一个数组a记录节点,且根结点下标为1
对于任一节点Tree[k]
它的左儿子为Tree[2*k](Tree[k<<1]);
它的右儿子为Tree[2*k+1](Tree[k<<1|1])
一维数组即实现了线段树节点信息的保存
建立一颗二叉树,并记录原数组的信息
void build(int id,int l,int r)//节点的编号,此节点代表的区间
{
tree[id].left=l;tree[id].right=r;
if(l==r)
{
Tree[id].maxx=a[l];
Tree[id].sum=a[l];
}
else
{
int mid=(l+r)>>1;
build(id<<1,l,mid);
build(id<<1|1,mid+1,r);//递归建树
Tree[id].maxx=max(Tree[id<<1].maxx,Tree[id<<1|1].maxx);//pushuup的操作,自下而上的操作
Tree[id].sum=Tree[id<<1].sum+Tree[id<<1|1].sum;//自下而上的操作
}
}
如果原数组从a[1]~a[n],调用build(1,1,n)即可;
线段树--跟新(跟新某个节点的数值,并维护相关点的信息)
void update(int id,int pos,int val)
{
if(Tree[id].left==Tree[id].right)//区间端点相等时,也就找到了要修改的数
{
Tree[id].sum=val;
Tree[id].maxx=val;
}
else
{
int mid=(tree[id].left+tree[id].right)>>1;
if(pos<=mid) update(id<<1,pos,val);
else update(id<<1|1,pos,val);
Tree[id].sum=Tree[id<<1].sum+Tree[id<<1|1].sum;//pushup
Tree[id].maxx=max(Tree[id<<1].maxx,Tree[id<<1|1].maxx)//pushuup
}
}
线段树--查询
查询区间内元素的和或最大值
这个区间完全包括的节点
int 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)>>1;
if(r<=mid)
return query(id<<1,l,r);
else if(l>mid)
return query(id<<1|1,l,r);
else
return query(id<<1,l,mid)+query(id<<1|1,mid+1,r)
}
}
调用query(1,l,r)即可查询[l,r]区间内元素的总和
懒操作在更新区间问题上至关重要,,lazy-tag,pushudown(向下)
线段树之延迟标记(区间修改)
看到这个问题可以立刻想到单点跟新加for循环,很慢,不可取。
延迟标记就是在递归过程中,如果当前区间被需要修改的区间完全覆盖,那么就要停止递归,
并且在上面做一个标记,但是这个信息没有更新到每一个元素(即叶子节点),
这个标记不仅仅是这个节点的性质,此性质作用于该节点的整个子树中,
假设我们另一个查询包含了当前区间的子孙区间,显然这个标记也要对之后的查询产生影响。
单点更新都是在叶节点中实现的,不会对后续节点产生影响,类比这种想法,lazy-tag应运而生
如果当前区间被需要修改的目标区间完全覆盖,打一个标记,如果下一次查询或修改包含此区间
那么,将这个标记分解,并且传递给其左右儿子,此标记去掉。
简单的来说,这个标记,在我们需要时才向下传递信息(pushdown),如果没有用到,则不再进行操作。
为了完成这个操作我们可以在结构体中增加一个add数组存储区间的延迟变化量(修改量)
5 4 3 2 1
[1 3]区间内每个数+2
查询[3 5]区间
[1 5](add=0,sum=15)
(add=0,sum=12)[1 3] [4 5](add=0,sum=3)
(add=0,sum=9[1 2] [3 3](add=0,sum=3) (add=0,sum=2)[4 4] [5 5](add=0,sum=1)
(add=0,sum=5[1 1] [2 2](add=0,sum=4)
[1 3]每个数+2
[1 5](add=0,sum=15)
(add=2,sum=12)[1 3] [4 5](add=0,sum=3)
(add=0,sum=9[1 2] [3 3](add=0,sum=3) (add=0,sum=2)[4 4] [5 5](add=0,sum=1)
(add=0,sum=5[1 1] [2 2](add=0,sum=4)
查询[3 5],在向下查找的过程中,发现有一个add值不等于0的点,于是进行pushudown操作下传标记
[1 5](add=0,sum=15)
(add=0,sum=12)[1 3] [4 5](add=0,sum=3)
(add=2,sum=9[1 2] [3 3](add=2,sum=3) (add=0,sum=2)[4 4] [5 5](add=0,sum=1)
(add=0,sum=5[1 1] [2 2](add=0,sum=4)