浅谈动态开点线段树
浅谈动态开点线段树
本篇随笔简单讲解一下线段树的常见优化技巧——动态开点。
要学动态开点首先得会线段树,如果不会的话,看官请走这边——
动态开点的概念和功能
现在要让你维护一棵值域为\(10^9\)的权值线段树。
掐指一算,按线段树开的话,四倍空间是\(4\times10^9\),空间必炸。
于是你开始怀疑这道题有问题,大骂出题人毒瘤
其实你只是不会动态开点。
所谓动态开点,就是用什么开什么。简单的说,就是建立一棵“残疾”的线段树,上面只有询问过的相关节点。从而节约了大量空间,让上面的\(10^9\)权值线段树成为可行。
其实看这个“动态开点”的名字就能隐约猜到这个概念和功能。
这样的话,我们最后的空间复杂度只与询问次数有关,也就是\(O(q\log N)\)。
动态开点的代码实现
先放代码,针对代码进行解释。
struct segment_tree
{
int sum,lson,rson;
}tree[maxq<<2];
int tot;
void update(int &pos,int l,int r,int k)
{
int mid=(l+r)>>1;
if(!pos)
pos=++tot;
if(l==r)
{
tree[pos].sum+=k;
return;
}
if(x<=mid)
update(tree[pos].lson,l,mid,k);
if(y>mid)
update(tree[pos].rson,mid+1,r,k);
tree[pos].sum=tree[tree[pos].lson].sum+tree[tree[pos].rson].sum;
}
int main()
{
int root=0;
update(root,1,maxn,k);
return 0;
}
大家会发现,这份代码较之原版的简单线段树并没有多大的差别,其中的精妙就在于节点编号的命名上。
原版线段树是一棵完整的二叉树,所以我们采取计算的方式来算出每个节点的左右儿子的编号,具体方式就是(这是条规律):左节点:当前节点×2,右节点:当前节点×2+1。
但是我们动态开点就不一样了。因为是一棵残疾的树,如果我们用这种计算节点编号的方式,就根本没有用了。所以我们这里所有的节点编号,都是人为规定的,玄机就在这个计数变量:tot上。
如果当前去到的节点pos为0,那就说明这个节点没有被使用过,tot+1变成编号给这个节点,与此同时,因为是递归回溯结构,所以这个时候的lson和rson都是已经被确定下来的。(其实tot就是表示当前已经开了多少节点)
这样,我们就总结出了动态开点的玄妙之处:就是节点的编号和节点左右儿子的编号都是乱序的,是我们临到使用之时现加上去的。
嘱咐几句:这个参数变量pos前面的取址符&是不能省略的,因为这个编号是不随递归修改的定值。
简单线段树可以用数组存,但是动态开点的必须用结构体,因为lson和rson是人为规定的。
差不多就这样?
祝同志们AK IOI!