“我一步一步走向深处,寻找你的身影,又将你拉回——我们的起点”
线段树是用来维护区间权值的一种数据结构,根据同机房巨佬而言,一般需要使用线段树的题考点都不在线段树上,不过线段树真滴好用
线段树的操作:
Ⅰ.建树
我的习惯为使用结构体存储(就和链式前向星一样),也可以分开打:
1 2 3 4 5
| struct SegmentTree { int l,r,dat; }Tree[N<<2];
|
一般来说,线段树的数组需要开四倍 $N$ 空间,因为他比线性结构要多一层,而开四倍一般不会爆。
代码讲解:
对于其中之一 $Tree[p]$ 中:
$l$ 表示第p个编号所表示的区域起点为原线性结构的 $A_l$ ;
$r$ 表示第p个编号所表示的区域终点为原线性结构的 $A_r$ ;
子节点保证: $l=r$ ;
$dat$ 表示该区域的一个数据,一般视题目而定,且 $dat$ 的个数也不定。
$tag$ 可有也可不有(如果是区间修改的题一般都有),表示“懒惰标记”,之后详解。
$p$ 结点的子结点为 $p2$ 和 $p2+1$ ,一般在代码中写为 $p<<1$ 和 $p<<1|1$ 来节约时间(因为位运算时间复杂度<乘法复杂度)
建树一般使用递归建树:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| inline void underBuild(int p,int l,int r) { Tree[p].l=l; Tree[p].r=r; if(l==r) { Tree[p].dat=Val[l]; return ; } int mid=(l+r)>>1; underBuild(p<<1,l,mid); underBuild(p<<1|1,mid+1,r); underPushUp(p); }
|
Ⅱ.查询
以查询该区间权值和为例:
如果该结点在需要查询的区间内,返回该点权值;
如果不在搜索子结点,如果需要的 $l$ 比该结点的 $mid$ 小,则搜索 $(l,mid)$;如果需要的 $r$ 比该结点的 $mid$ 大,则搜索$(mid+1,r)$。
具体操作看函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| inline long long underQuery(int p,int l,int r) { if(Tree[p].l>=l&&r>=Tree[p].r) { return Tree[p].dat; } underSpread(p); int mid=(Tree[p].l+Tree[p].r)>>1; long long val=0; if(l<=mid) { val+=underQuery(p<<1,l,r); } if(mid<r) { val+=underQuery(p<<1|1,l,r); } return val; }
|
Ⅲ.单点修改
找到该点的编号,从下至上修改它的所有父结点直到,依然使用递归操作。
$dat$ 存储该区间的区间和:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| inline void underChange(int p,int v,int k) { if(Tree[p].l==v&&Tree[p].r==v) { Tree[p].dat=k; return ; } int mid=(Tree[p].l+Tree[p].r)>>1; if(v<=mid) { underChange(p<<1,l,r,k); } if(v>mid) { underChange(p<<1|1,l,r,k); } Tree[p].dat=Tree[p<<1].dat+Tree[p<<1|1].dat; }
|
Ⅳ.向上传递(pushup)
即函数underPushUp,一般都是视题目而定。
以区间和为例:
1 2 3 4
| inline void underPushUp(int p) { Tree[p].dat=Tree[p<<1].dat+Tree[p<<1|1].dat; }
|
似乎这个例子没什么说服力,不过的确是这样的。
Ⅴ.向下传递(pushdown/spread)
只限于懒惰标记。当我们需要改变该区间中的子区间才会向下传递,这样可以优化时间,否则每一次区间修改都会花费 $O(N)$ 的时间复杂度。
依然以区间和为例:
1 2 3 4 5 6 7 8 9 10 11
| inline void underSpread(int p) { if(Tree[p].tag) { Tree[p<<1].dat+=Tree[p].tag*(Tree[p<<1].r-Tree[p<<1].l+1); Tree[p<<1|1].dat+=Tree[p].tag*(Tree[p<<1|1].r-Tree[p<<1|1].l+1); Tree[p<<1].tag+=Tree[p].tag; Tree[p<<1|1].tag+=Tree[p].tag; Tree[p].tag=0; } }
|
Ⅵ.区间修改
使用懒惰标记,可以让时间复杂度降到 $O(logN)$ 。
依然以区间和为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| inline void underModify(int p,int l,int r,int k) { if(l<=Tree[p].l&&Tree[p].r<=r) { Tree[p].dat+=k*(Tree[p].r-Tree[p].l+1); Tree[p].tag+=k; return ; } underSpread(p); int mid=(Tree[p].l+Tree[p].r)/2; if(l<=mid) { underModify(p<<1,l,r,k); } if(r>mid) { underModify(p<<1|1,l,r,k); } Tree[p].dat=Tree[p<<1].dat+Tree[p<<1|1].dat; }
|
一些练习
区间修改区间查找模板题
区间最大子段和模板题
P2574 XOR的艺术
区间求sin和,需要使用数学知识
卡常卡码风的毒瘤题
头图来源:
Warma