————————————18.4.18更新
有时我们遇到一些非常恶心神犇的题:数据量又大,又要做在线修改在线返值的题,显然这时我们不可能用O(n)的数组一个一个地去修改,去求值。
这时,我们就需要用到线段树了!!!!!!
让我们来看看吧
线段树的简介
我们设置一个二叉树,并定义根节点的区间范围是(1,n)。
我们设一个点的区间是(l,r),则每个点的左儿子的区间范围是(l,(l+r)/2),右儿子的区间范围是((l+r)/2+1,r)。同时,它的左儿子的编号是它的编号的2倍,右儿子是编号2倍加一。
这样,我们就得到了一棵线段树了。。。请注意,它是一个严格的二叉平衡树
每个节点的都存储它表示区间的各种信息,如:该区间的最值,所有点的和,乘积。。。。而一个节点的值我们可以根据它的两个儿子的值求出来。
接下来的操作,如:求和,求最值,求乘积,修改区间等就可以在树上操作了,具体实现会在后面讲到
线段树的优劣
首先,线段树树的修改和查询都是o(log2n)的。所以说相比之前的暴力这就很coooooool了。
线段树的空间复杂度是o(n)的,但是实际上空间是n*8的,所以相比某些算法要差劲些。(例如,离线时差分数组(而且差分数组的空间仅为o(n))求和更厉害,倍增法求最大更厉害)
但是线段树是在线的,这就是它的优越性。树状数组也是在线的,常数比它小,但是线段树可以求最值,所以是很优秀的!!!!
线段树的代码实现
建树:在开始计算时,我们的线段树还没建起,所以需要初始化一下。
struct node {
LL sum,l,r,lazy,maxx; //maxx表示该区间的最大值,sum表示该区间的值,l,r表示该区间的左右界,lazy表示懒值
} a[maxn];
void btree(LL l,LL r,LL x) { //l,r表示目前点的左右界,x表示目前节点的编号
if(l==r) { //若该点的左右界相同了,则不往下继续建树了
a[x].l=l;
a[x].r=r;
a[x].sum=s[l]; //该点的和就是它的初始值
a[x].maxx=a[x].sum;//该点的最值就是它的初始值
return;
}
LL p=x<<1;
a[x].l=l;
a[x].r=r;
btree(l,(l+r)>>1,p); //建立左儿子,其编号为2*x
btree(1+((l+r)>>1),r,p+1); //建立右儿子,其编号为2*x+1
a[x].sum=a[p].sum+a[p+1].sum;//它的区间值等于左儿子区间值加右儿子区间值
a[x].maxx=max(a[p].maxx,a[p+1].maxx); //它的最值等于左儿子最值与右儿子最值的最值
}
修改:我们要修改某个区间的值,如将某区间的所有点加某值。
如果暴力修改所有被包含区间后,我们发现在数据很大时会超时,这是为什么呢?因为修改某个区间我们发现要逐个修改该区间的所有树点,这样就无法发挥线段树的作用。这时其实我们先将修改时遇到的点权先更新一下,保证这个点的祖先们的值能得到更新,再给它的左右儿子上累计博主曾经因为这个改了一天的代码懒值,当我们询问到有懒值的点时,我们就下方懒值即可。(注:该懒值思想在后面用得非常多,请一定深刻理解)
void put(LL x){//下放操作
a[x].sum+=a[x].lazy*(a[x].r-a[x].l+1);//该点的和就是该点值加上它的区间长度与懒值的乘积
a[x].maxx+=a[x].lazy;//该区间的最大值就是它的最大值加上懒值
a[x<<1].lazy+=a[x].lazy;//累计左儿子的懒值
a[(x<<1)+1].lazy+=a[x].lazy;//累计右儿子的懒值
a[x].lazy=0;//该点下放完成 ,懒值为0
}
void modify(LL x,LL l,LL r,LL q) {//q表示区间全部点加上q
if(a[x].r<l||r<a[x].l)return;//如果询问到的区间左右区间超出的,就不再修改。
if(a[x].lazy)put(x); //遇到有懒值就下放(注:此处可不下放,博主为了保险起见就修改了)
if(l<=a[x].l&&a[x].r<=r) {//如询问区间被修改区间完全被包含 ,则开始修改
a[x].lazy+=q;//加上懒值
put(x); //下方更新该点
return ;
}
LL p=x<<1;
modify(p,l,r); //更新左儿子
modify(p+1,l,r); //更新右儿子
a[x].sum=a[p].sum+a[p+1].sum;
a[x].maxx=max(a[p].maxx,a[p+1].maxx);
}
查询:查询某个区间的和(最大值请自行思考博主懒得写了)
LL getsum(LL x,LL l,LL r) {//查询区间和
if(a[x].r<l||r<a[x].l)return 0; //如果该区间超出查询区间时,则返回0
if(a[x].lazy)put(x);// 遇到有懒值就下放
if(l<=a[x].l&&a[x].r<=r) {
return a[x].sum; // 如询问区间被修改区间完全被包含 ,则返回该区间的和
}
LL p=x<<1,sl=getsum(p,l,r),sr=getsum(p+1,l,r); //求左右所有儿子的和值
return sr+sl; //返回该点的左右儿子区间和的值
}
总结线段树:
博主打字很辛苦,希望各位神犇能够多多光顾该博。。谢谢