学习笔记 --- 线段树
开始切割数据结构,种树之路,学习了一下线段树,写一下心得与体会
一线段树摆上:
线段树的原理:将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。
线段树中有如下定理:
定理:n>=3时,一个[1,n]的线段树可以将[1,n]的任意子区间[L,R]分解为不超过个子区间。
定理:n>=3时,一个[1,n]的线段树可以将[1,n]的任意子区间[L,R]分解为不超过个子区间。
一般存储采用数组直接存储:
当前点的位置为now,那么它的左子树位置为now*2,右子树位置为now*2+1(和二叉树存储同理)
那么是线段树的模板(此处以一个维护和的线段树演绎)
建树过程:
void updata(int now)
{
sum[now]=sum[now<<1]+sum[now<<1|1];
}//求和过程
//此过程线段树核心过程,一般不同的线段树,最大的不同处就在此处
void build(int left,int right,int now)
{
if (left==right)
{
sum[now]=tree[left];
return;
}//每当达到叶节点就把值更新一下
int mid=(left+right)>>1;
build(left,mid,now<<1);//左子树建树
build(mid+1,right,now<<1|1);//右子树建树
updata(now); //容易遗忘但是特别重要的一个语句,在更新完子数后根据子数修改根
}//建树过程
点修改过程:
更新一个点的值
void point_change(int now,int left,int right,int data,int locate)
{
if (left==right)
{
sum[now]+=data;
return;
}//如果到达这个点的位置,更新
int mid=(left+right)>>1;
if (locate<=mid)//如果要修改的点位于左子树,向左查询
point_change(now<<1,left,mid,data,locate);
else//反之向右
point_change(now<<1|1,mid+1,right,data,locate);
updata(now);//仍旧不能忘,更新完点就要更新根
} //点修改过程
区间修改过程:
更新区间【left,right】的值
标记的含义:
本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。即,如果要给一个区间的所有值都加上1,那么,实际上并没有给这个区间的所有值都加上1,而是打个标记,记下来,这个节点所包含的区间需要加1.打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。
标记有相对标记和绝对标记之分:
相对标记是将区间的所有数+a之类的操作,标记之间可以共存,跟打标记的顺序无关(跟顺序无关才是重点)。
所以,可以在区间修改的时候不下推标记,留到查询的时候再下推。绝对标记是将区间的所有数变成a之类的操作,打标记的顺序直接影响结果,所以这种标记在区间修改的时候必须下推旧标记,不然会出错。
注意,有多个标记的时候,标记下推的顺序也很重要,错误的下推顺序可能会导致错误。
void section_change(int LEFT,int RIGHT,int data,int left,int right,int now)//LEFT,RIGHT表示需要更新的区间,data为更新值,left,right为当前点的控制区间,now当前位置
{
if (LEFT<=left && RIGHT>=right)//如果本区间完全在更新的区间范围,就更新
{
sum[now]+=data*(right-left+1);
delta[now]+=data;//惰性标记,子区间的值需要依此修改
return;
}
int mid=(left+right)>>1;
if (LEFT<=mid)//判断左子树和修改区间有无交集,有交集则递归
section_change(LEFT,RIGHT,data,left,mid,now<<1);
if (RIGHT>mid)//原理同上
section_change(LEFT,RIGHT,data,mid+1,right,now<<1|1);
updata(now);//同理需要更新根
}//区间修改过程
区间查询过程:
查询区间【left,right】的值
void pushdown(int now,int leftn,int rightn)//把根的标记下放到子数,确保答案的正确性
{
if (delta[now]!=0)
{
delta[now<<1]+=delta[now];
delta[now<<1|1]+=delta[now];//标记下推
sum[now<<1]+=delta[now]*leftn;
sum[now<<1|1]+=delta[now]*rightn;//修改子节点的值
delta[now]=0;//清除当前点的标记
}
}//标记下放函数
int query(int LEFT,int RIGHT,int left,int right,int now)
{
if (LEFT<=left && RIGHT>=right)
return sum[now];//在区间内,直接返回即可
int mid=(left+right)>>1;
pushdown(now,mid-left+1,right-mid);//标记下推(不加这步,结果出错)
int total=0;//累计答案
if (LEFT<=mid) total+=query(LEFT,RIGHT,left,mid,now<<1);
if (RIGHT>mid) total+=query(LEFT,RIGHT,mid+1,right,now<<1|1);
return total;
} //区间查询
主过程的调用:
int main()
{
//建树
Build(1,n,1);
//点修改
Update(L,C,1,n,1);
//区间修改
Update(L,R,C,1,n,1);
//区间查询
int ANS=Query(L,R,1,n,1);
}
说了这么多,还是需要大量题目的练习( ⊙ o ⊙ )啊!…..