线段树数据结构详解
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(ncdot 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
。