线段树
第一部 概念引入
线段树是一种二叉树,也就是对于一个线段,我们会用一个二叉树来表示。比如说一个长度为4的线段,我们可以表示成这样:
性质:节点i的权值=她的左儿子权值+她的右儿子权值。
我们知道,一颗二叉树,她的左儿子和右儿子编号分别是她*2和她*2+1
再根据刚才的性质,得到式子:tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;就可以建一颗线段树了!
1.建树:
代码如下:
//建树
void BuildTree(int left,int right,int i){
tree[i].L=left;
tree[i].R=right;
if(left==right){//叶子节点
return ;
}
int mid=(left+right)>>2;
//递归构建左右子树
BuildTree(left,mid,i*2);
BuildTree(mid+1,right,i*2+1);
tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;
}
2.区间查询
我们总结一下,线段树的查询方法:
1、如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
2、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
3、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子
代码:
//区间查询
int search(int left,int right,int i){
//如果这个区间被完全包括在目标区间里面,直接返回这个区间的值
if(tree[i].L>=left&&tree[i].R<=right){
return tree[i].sum;
}
//如果这个区间和目标区间毫不相干,返回0
if(tree[i].R<left||tree[i].L>right)return 0;
int sum=0;
//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子
if(tree[i*2].R>=left) sum+=search(left,right,i*2);
//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子
if(tree[i*2+1].L<=right) sum+=search(left,right,i*2+1);
return sum;
}
3.区间点修改
//区间点修改,区间点 dis 加 k
void add(int i,int dis,int k){
if(tree[i].L==tree[i].R){//如果是叶子节点,那么说明找到了
tree[i].sum+=k;
return ;
}
if(dis<=tree[i*2].R)
add(i*2,dis,k);//在左子树
else
add(i*2+1,dis,k);//在右子树
tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//返回更新
return ;
}
4.区间修改
区间修改和单点查询,我们的思路就变为:如果把这个区间加上k,相当于把这个区间涂上一个k的标记,然后单点查询的时候,
就从上跑道下,把沿路的标记加起来就好。这里面给区间贴标记的方式与上面的区间查找类似,原则还是那三条,只不过第一条:
如果这个区间被完全包括在目标区间里面,直接返回这个区间的值变为了如果这个区间如果这个区间被完全包括在目标区间里面,讲这个区间标记k。
// 区间修改
void add(int i,int left,int right,int k){
if(tree[i].L>=left && tree[i].R<=right){//如果这个区间被完全包括在目标区间里面,将这个区间标记k
tree[i].sum+=k;
return ;
}
//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子
if(tree[i*2].R>=left)
add(i*2,left,right,k);
//如果这个区间的右儿子和目标区间又交集,那么搜索右儿子
if(tree[i*2+1].L<=right)
add(i*2+1,left,right,k);
}
6.单点查询
然后就是单点查询了,这个更好理解了,就是dis在哪往哪跑,把路径上所有的标价加上就好了:
//单点查询
int ans=0;
void search(int i,int dis){
ans+=tree[i].sum;//一路加起来
if(tree[i].L==tree[i].R)
return ;
if(dis<=tree[i*2].R)
search(i*2,dis);
if(dis>=tree[i*2+1].L)
search(i*2+1,dis);
}
进阶线段树
区间修改、区间查询,你可能会认为,把上一章里面的这两个模块加在一起就好了,然后你就会发现你大错特错。
因为如果对于1~4这个区间,你把1~3区间+1,相当于把节点1~2和3标记,但是如果你查询2~4时,你会发现你加的时没有标记的2节点和没有标记的3~4节点加上去,结果当然是错的。
那么我们应该怎么办?这时候pushdown的作用就显现出来了。
你会想到,我们只需要在查询的时候,如果我们要查的2节点在1~2区间的里面,那我们就可以把1~2区间标记的那个+1给推下去这样就能顺利地加上了。
怎么记录这个标记呢?我们需要记录一个“懒标记”lazytage,来记录这个区间
区间修改的时候,我们按照如下原则:
1、如果当前区间被完全覆盖在目标区间里,讲这个区间的sum+k*(tree[i].r-tree[i].l+1)
2、如果没有完全覆盖,则先下传懒标记
3、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子
4、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子