线段树数据结构详解
Coded by Jelly_Goat. All rights reserved.
这一部分是线段树。
线段树,顾名思义,是一种树形数据结构,适用于各种求区间统一算法的动静两平衡的数据结构。
这里什么是统一算法?(自己口胡的统一算法)
比如求最大值or最小值、区间求和,一样的区间都是一样的算法,这也是和动态dp不同的地方。
前置知识1:二叉搜索树
二叉搜索树就是根节点比左儿子大,比右儿子小的一种二叉树。
前置知识2:向量存储
向量存储是用来存完全二叉树儿子和父亲关系的。如果不满足,我们还可以用链式前向星存
举个例子:
有一颗完全二叉树,节点数是16,然后你会发现:lson标号=root标号*2
,rson标号=root标号*2+1
。
显然可见不是偶然,是二叉树满了导致的。
那么我们可以用下标表示存储的线段树节点。
例如:tree[100]
就是tree[200]
和tree[201]
的root。
今天只讨论最普通的线段树(板子:求和)
操作1:建树
怎样种一棵线段树?Jelly_Goat:需要一条线段
- 没问题,真的需要原序列。
- 从上往下二分区间长度,递归建树。
代码示范:
//维护根节点的和 inline void update(int rt) { tree[rt].sum=tree[rt*2].sum+tree[rt*2+1].sum; } //建树过程 //递归建议不要加inline //根节点标号,左端点,右端点 void build_tree(int rt,int l,int r) { //为tree复制左右端点 tree[rt].l=l,tree[rt].r=r; if (l==r) { //如果已经是一个点,就输入数据sum scanf("%d",&tree[rt].sum); //一个暂时性标记 tree[rt].tag=0; //返回 return; } int mid=(r+l)/2;//是中间节点 build_tree(rt*2,l,mid);//二分区间 build_tree(rt*2+1,mid+1,r); update(rt);//加和 }
树高是logn的,
因此一次建树操作是\(O(n\cdot logn)\)的。
操作2:查询单点、修改单点
充分利用线段树是二叉搜索树的特点。
此话怎讲?
我们可以将点和线段中点比较啊qwq
if 在左半边 搜索半边
else 右半边同理
找到了就返回sum值即可。
修改完了以后可以进行一个update维护线段树的值。
代码示范:
//根节点,点的位置,此点加上num void change_p(int rt, int p, lli num) { //即现在是一个点,即我们要找的p点 if (tree[rt].l == tree[rt].r) { //修改 tree[rt].sum += num; //返回 return; } //线段中点 int mid = (tree[rt].l + tree[rt].r) >> 1; if (tree[rt].tag)//如果有缓存,清理一下(待会说这个是怎么回事 pushdown(rt); if (p <= mid)//左半边 change_p(rt << 1, p, num); else//右半边 change_p((rt << 1) + 1, p, num); update(rt);//更新和 } //根节点标号,点 lli ask_p(int rt, int p) { //同修改的道理,这里就不加注释了 if (tree[rt].l == tree[rt].r) { return tree[rt].sum; } if (tree[rt].tag) pushdown(rt); int mid = (tree[rt].l + tree[rt].r) >> 1; if (p <= mid) return ask_p(rt << 1, p); else return ask_p((rt << 1) + 1, p); }
因为树高是logn的,所以每一次最多搜到logn次深度。
所以复杂度是\(O(logn)\)的。
操作三:区间修改、区间查询
一开始我们可以暴力一点,将区间拆成一个个点。
但是区间一长了,这个操作就炸了,相当于重新建了一棵树...
所以这里涉及到一个问题:线段树,怎样发挥线段的作用?
是的,整体操作。
我们加一个缓存tag,属于lazy算法。
我们每一次匹配到一个线段,都给其进行一个缓存操作而不是向下传递更改,直到这个节点被用到。
被用到,意味着被查看、修改。
这样我们将最坏的时间复杂度降到了\(O(logn)\)级别的,因为最坏情况就是半边覆盖加上一个点进行修改。
代码示范:
//根节点标号,左端点,右端点,加上num void change_seg(int rt, int l, int r, lli num) { //如果区间完全覆盖,则进行缓存 if (tree[rt].l == l && tree[rt].r == r) { tree[rt].tag += num; //加上缓存 tree[rt].sum += (tree[rt].r - tree[rt].l + 1) * num; //整体的和即加上区间长度*num return; } if (tree[rt].tag)//有缓存就清空 pushdown(rt); int mid = (tree[rt].l + tree[rt].r) >> 1;//中点 if (r <= mid)//完全都在左半边 change_seg(rt << 1, l, r, num); else if (l > mid)//完全都在右半边 change_seg((rt << 1) + 1, l, r, num); else//两边都有 { change_seg(rt << 1, l, mid, num); change_seg((rt << 1) + 1, mid + 1, r, num); } update(rt);//更新和 } //根节点标号,左端点,右端点 lli ask_seg(int rt, int l, int r) { //类似查询不再赘述 if (tree[rt].l == l && tree[rt].r == r) { return tree[rt].sum; } if (tree[rt].tag) pushdown(rt); int mid = (tree[rt].l + tree[rt].r) >> 1; if (r <= mid) return ask_seg(rt << 1, l, r); else if (l > mid) return ask_seg((rt << 1) + 1, l, r); else return ask_seg(rt << 1, l, mid) + ask_seg((rt << 1) + 1, mid + 1, r); }
操作4:清除缓存
那当然\(O(1)\)处理这个问题。
直接上代码,自己去理解。
inline void pushdown(int rt) { int lson = rt << 1, rson = lson + 1; tree[lson].tag += tree[rt].tag; tree[rson].tag += tree[rt].tag; tree[lson].sum += (tree[lson].r - tree[lson].l + 1) * tree[rt].tag; tree[rson].sum += (tree[rson].r - tree[rson].l + 1) * tree[rt].tag; tree[rt].tag = 0; }
完成。
完整的代码在GitHub开源:transport
。
来源:https://www.cnblogs.com/jelly123/p/10743601.html