「线段树学习笔记」
前言
写得确实挺烂的,当时码风也不怎么样,不喜勿喷.
线段树
线段树的作用&原理
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logn)。而未优化的空间复杂度为2n,实际应用时一般还要开4n的数组以免越界,因此有时需要离散化让空间压缩。
以上内容摘抄自百度百科.
一般来说线段树长这样(画的有点丑),每一个节点(非叶节点)都是从它的两个子树的根节点合并而成,所以线段树维护的值必须支持合并(具体下面会讲到),对于每一次的修改可以直接修改若干颗子树,可以证明最多修改的子树不会超过\(\log_2n\)棵,所以每次的修改的时间复杂度为\(\log_2n\),查询同理.
可以在 \(\mathcal{O}(\log_2n)\) 的复杂度内修改和查询一定的信息(如区间加一个数,查询区间和,查询区间最大值等).
线段树的实现
define
以下为本文中绝大部分define.
#define LSON now*2;
#define RRSON now*2+1
#define MIDDLE (left+right)/2
#define LEFT LSON,left,MIDDLE
#define RIGHT RRSON,MIDDLE+1,right
#define now now_left,now_right
先从最基础的开始例题1
合并信息
线段树最先要写的部分是合并信息.(个人习惯)
这里要维护的只有一个区间和,那么合并就很简单了.
void PushUp(int now)
{
sgt[now].sum=sgt[LSON].sum+sgt[RRSON].sum;
}
建树
写完合并,接下来要建树.(不是所有的线段树都需要建树)
建树时最多也就只会有 \(n*4\) 个节点,所以建树的复杂度是 \(\mathcal{O}(n)\) 的.
void Build(int now=1,int left=1,int right=n)//建树的初始值固定
{
if(left==right)//叶节点时直接赋值
{
sgt[now].sum=arr[left];
return;
}
Build(LEFT);//建左子树
Build(RIGHT);//建右子树
PushUp(now);//建完后需要合并
}
Lazy标记&PushDown
lazy标记可以说是线段树的精髓所在,可以让修改只有当查询到或者再次修改时才会真正去修改值,使得复杂度大大降低.
如图,需要修改蓝色区域的值,那么它覆盖的部分为红色的两颗子树,但是绿色的位置的值也发生了改变,这时就需要在红色位置打上lazy标记,lazy标记也要支持合并,在以后的修改时需要查询(修改)到绿色部分时才会将标记下传.
如图,需要查询紫色部分的值,那么如果需要查询蓝色部分的那两颗子树的值,这时就需要将红色位置的标记下传,得到蓝色部分的真实的值.
void Down(int now,int left,int right,int lazy_)//修改子树
{
sgt[now].sum+=(right-left+1)*lazy_;//子树表示的值需要加上子树长度*每个数增加的值
lazy[now]+=lazy_;//子树懒标记增加
}
void PushDown(int now,int left,int right)
{
Down(LEFT,lazy[now]);//修改左子树
Down(RIGHT,lazy[now]);//修改右子树
lazy[now]=0;//lazy标记必须清空
}
修改
修改部分并没有什么可以说的,只需要将区间内最上层的所有节点都打上标记就可以了.
void Updata(int now_left,int now_right,int num,int now=1,int left=1,int right=n)
//其中的now,left,right一般不会变,所以就缺省了
{
if(left>now_right||now_left>right)return;//如果需要查询的区间与当前区间没有公共位置则推出
if(now_left<=left&&right<=now_right)//如果需要查询的区间包含了当前区间可以直接修改
{
sgt[now].sum+=num*(right-left+1);
lazy[now]+=num;//注意修改懒标记
return;
}
PushDown(now,left,right);//需要推一下懒标记为了下方的修改
Updata(now,num,LEFT);//修改左子树
Updata(now,num,RIGHT);//修改右子树
PushUp(now);//合并
}
查询
查询与修改类似.
int Query(int now_left,int now_right,int now=1,int left=1,int right=n)
{
if(left>now_right||now_left>right)return 0;//不在范围内
if(now_left<=left&&right<=now_right)//包含了
{
return sgt[now].sum;
}
PushDown(now,left,right);//下传懒标记
return Query(now,LEFT)+Query(now,RIGHT);//需要将左右子树的值相加
}
权值线段树
权值线段树的的原理&作用
权值线段树其实很简单,其实就是下标为值域的一颗线段树.
可以很容易得出整个数列第k大值,只需要在树上二分就行了.
也可以处理一些区间中出现次数最多的数的个数之类的问题.
例题
权值线段树的代码实现
单点修改
void Updata(int num,int now=1,int left=1,int right=n)
{
if(num>right||num<left)
{
return;
}//按这个数的大小左右二分,最多Logn次就到叶节点,所以每次修改的时间复杂度为O(logn)
sgt[now]++;//可以直接修改
if(left==right)
{
return;
}
Updata(num,LEFT);
Updata(num,RIGHT);
}
查询第k大
int Query(int num,int now=1,int left=1,int right=n)
{
if(left==right)//到叶节点说明找到了
{
return left;
}
if(sgt[LSON]>=num)
{
return Query(num,LEFT);//如果在左子树就往左子树找
}
return Query(num-sgt[LSON],RIGHT);//不在就往右子树找
}
对于一些很大数可能需要先离散化.
动态开点
动态开点的作用&原理
动态开点是线段树中的一个基础知识,在线段树合并,分裂,主席树等地方都是必须用到的.
在一些用到权值线段树的题目中数据如果数据大于1e6基本就会跑不过了,但是,有了动态开点,可以节省很多用不到的点.
如图,在这样一颗线段树中,灰色部分的节点时没有用的如果在权值线段树中像这样灰色的节点的数量可能会很多,这样极大的浪费了空间,所以需要用一种新的方式来存这棵树,动态开点就是这样出现的.
在原本的线段树中节点 \(x\) 的左儿子为 \(x\times2\) 右儿子为 \(x\times2+1\).而现在,不再通过计算的方式获得子节点编号,需要直接记录在当前节点上,每次修改时如果需要修改到一个没有加入的节点时才会将这个节点放入这颗树中,这样可以节省很多的空间.
动态开点的代码实现
大体与普通线段树类似,但是在带修改(下传标记)的部分需要传入 \(now\) 的地址.
//以下部分需要更改
#define LSON sgt[now].lson
#define RRSON sgt[now].rson
struct SegmentTree
{
int lson,rson;//记录当前节点左右子节点
long long sum;
}
下传标记
void Down(int &now/*在Down时会修改部分的值,所以需要传入地址*/,int left,int right,int add)
{
if(!now)
{
now=++cnt;//如果没有这个点就加入这个点
}
sgt[now].sum+=(right-left+1)*add;
lazy[now]+=add;
}
void PushDown(int now,int left,int right)
{
if(lazy[now])//如果不加在根本没有需要下传的标记时可能会多开很多的点
{
Down(LEFT,lazy[now]);
Down(RIGHT,lazy[now]);
lazy[now]=0;
}
}
修改
void Updata(int now_left,int now_right,int add,int &now/*这里也需要传入一个地址*/,int left=1,int right=n)
{
if(now_left>right||left>now_right)
{
return;
}
if(!now)
{
now=++cnt;//如果没有这个点就加入这个点
}
if(now_left<=left&&right<=now_right)
{
sgt[now].sum+=(right-left+1)*add;
lazy[now]+=add;
return;
}
PushDown(now,left,right);
Updata(now,add,LEFT);
Updata(now,add,RIGHT);
PushUp(now);
}
线段树合并
线段树合并的原理&作用
线段树合并基于动态开点,所以需要先学习有关动态开点的内容后再看一下这部分内容
在一些题目中需要将两颗线段树和在一起,组成一棵更大的线段树,这时就需要用到线段树合并.
在如图所示的两颗线段树中(线段树为动态开点,灰色部分为没有值),如果把这两颗线段树合并后为:
在有值的位置将两颗线段树的值相加,如果有一棵线段树这一个位置没有值则为另一棵线段树的值.
可以将两颗线段树和为一棵线段树,看起来可能没什么用,可以先看看这道例题,需要查询第 \(k\) 大值,这就很容易想到在权值线段树二分了,但是,对于将岛屿之间两边的操作时,就需要用到线段树合并了,对于每个岛开一棵权值线段树,维护每种数出现的次数,在不同的岛相连时就将所属的线段树合并(至于合并时两座岛屿所属线段树的 \(root\) 可以用并查集轻松维护),这样就可以轻松切掉这题了.
线段树合并的代码实现
合并
以下代码为将 \(sgt2\) 合并到 \(sgt1\) 上.
void Merge(int &sgt1/*因为sgt1是要修改的,所以需要传入地址*/,int sgt2,int left=1,int right=n)
{
if(!sgt1||!sgt2)//如果在两颗合并的子树中有一颗为空树,那么就直接是可能非空(有可能是两颗空树)的那棵
{
sgt1+=sgt2;
return;
}
if(left==right)//当合并到叶节点时就直接将值合并即可
{
sgt[sgt1].sum=sgt[sgt1].sum+sgt[sgt2].sum;
return;
}
//递归合并子树
Merge(sgt[sgt1].lson,sgt[sgt2].lson,left,MIDDLE);
Merge(sgt[sgt1].rson,sgt[sgt2].rson,MIDDLE+1,right);
PushUp(sgt1);//将合并后的线段树上的值合并
}
线段树合并的复杂度
可以发现线段树合并时如果两颗树都有的部分需要全部扫一遍,有一棵树没有的部分可以直接返回,所以它的复杂度为两树在同一位置都有节点的节点的个数,而一颗树中只有\(n*4\)个节点,所以线段树合并的时间复杂度为\(O(n)\).
以上内容纯属一个不会线段树的菜鸡扯淡,不要听这个菜鸡瞎bb.
关于线段树合并的复杂度证明,可以发现在线段树合并中每一次都会删除一个节点,所以说最多只会删掉产生的节点数,所以线段树合并的复杂度的上限为在其他操作时最多能产生的节点数,所以这个复杂度是均摊的,而并非是严格的 \(n\log_2n\),需要按其他会产生新节点的操作计算复杂度.
线段树空间回收
空间回收的作用&原理
可以发现在线段树合并中会浪费掉很多的空间(每次合并节点时就会有一个节点被浪费掉),在比赛中每一点的空间都是极为珍贵,于是就出现了线段树的空间回收.
可以将一些不再会有用的点先存放起来,在未来需要开出一个新的节点时可以先在这些被"扔掉"的点中取,在线段树合并(可能有一些其他操作)时可以节省大量的空间.
空间回收的代码实现
删除节点
void Delete(int &now/*传的是地址*/)
{
sgt[now].lson=sgt[now].rson=0;
sgt[now]./*这个节点中的内容都要删除*/;
rubbish[++tot]=now;//可以将这个rubbish数组理解为一个栈,tot为栈顶
now=0;//顺便删掉这个位置
}
新建节点
int New()
{
if(tot)//如果"垃圾桶"内有元素,那么就从"垃圾桶"里拿
{
return rubbish[tot--];
}
return ++cnt;//否则就拿一个新节点
}
空间回收只能用在一些特殊的线段树中(主要是线段树合并).
线段树分裂
线段树分裂的原理
找了很久也没有什么特别好的题和博客,于是自己yy出了一道简单模板题.
对于图中这样的一棵线段树,如果需要分裂出其中橙色的部分,就需要新建几个祖先节点(绿色部分),需要把原来的线段树
中与这颗子树有关的边断开(红色线段断开的边).
线段树分裂的代码实现
分裂
void Split(int &sgt1,int &sgt2,int now_left,int now_right,int left=1,int right=n)
//在sgt1这棵权值线段树中把left~right的部分分裂到sgt2中
{
if(right<now_left||now_right<left)//不在需要分裂的范围内
{
return;
}
if(!sgt1)//如果在sgt1不存在这颗子树那么自然就没有了
{
return;
}
if(now_left<=left&&right<=now_right)//如果在范围内就直接断开sgt1中与父亲的边,并且连到sgt2的下面
{
sgt2=sgt1;//直接连到sgt2中
sgt1=0;//把这条边断开
return;
}
if(!sgt2)
{
sgt2=New();//如果不被修改范围的区间包含的部分需要新建一个节点(绿色部分)
}
//继续向下分裂
Split(sgt[sgt1].lson,sgt[sgt2].lson,now,left,MIDDLE);
Split(sgt[sgt1].rson,sgt[sgt2].rson,now,MIDDLE+1,right);
PushUp(sgt1);//最后需要更新维护的信息
PushUp(sgt2);
}
线段树分裂的时间复杂度
这个东西和线段树的区间修改极其相似,所以可以用同样的方法来证明复杂度,可以发现这个东西是 \(\mathcal{O}(n\log_2n)\) 的,且最多会产生 \(n\log_2n\) 个新节点.
标记永久化
标记永久化的作用&原理
普通的线段树需要 PushUp()
和 PushDown()
,标记永久化就可以不需要这两个东西了.
标记永久化就是对于每次修改就在最上层的节点上打上标记,在查询时就只需要一路向下查询将标记加上就可以得到答案了.
标记永久化的代码实现
节点
struct SegmentTree
{
long long sum;
long long tag;//需要加上一个标记用的量,但是不用懒标记
}sgt[maxn*4];
下传标记
它 SPFA 了
修改
void Updata(int now_left,int now_right,int add,int now=1,int left=1,int right=n)
{
if(now_right<left||right<now_left)
{
return;
}
sgt[now].sum+=1ll*(min(now_right,right)-max(now_left,left)+1)*add;
//修改的区间对于当前区间的贡献
if(now_left<=left&&right<=now_right)
{
sgt[now].tag+=add;//被完全包含了就直接修改tag
return;
}
Updata(now,add,LEFT);
Updata(now,add,RIGHT);
//PushUp也SPFA了
}
查询
long long Query(int now_left,int now_right,int now=1,int left=1,int right=n)
{
if(now_right<left||right<now_left)
{
return 0;
}
if(now_left<=left&&right<=now_right)
{
return sgt[now].sum;//包含就直接返回线段树上的值
}
return 1ll*(min(now_right,right)-max(now_left,left)+1)*sgt[now].tag//当前节点的tag对于这次查询的贡献
+Query(now,LEFT)+Query(now,RIGHT);//继续查询左右区间内的值
}
不是所有的修改都可以标记永久化,而且线段树可持久化线段树中用懒标记的复杂度也是正确的(知识需要更大的空间),所以这个东西更加没啥用了.
二维线段树
二维线段树的作用&原理
二维线段树有两种写法,四分树和树套树,根据dalao的说法,四分树单词修改的复杂度是会被成 \(\mathcal{O}(n)\) 的,所以以下内容主要讲树套树的写法.
顾名思义,二维线段树是用来维护二维平面上的东西,这里的树套树是在线段树的每个节点再开一棵线段树,这样每次修改和查询的时间复杂度就可以做到 \(\mathcal{O}(n\log^2_2n)\).
二维线段树的代码实现
拿出一道模板题.
大致题意就是查询矩阵最大值+矩阵覆盖,对于线段树其实没什么变化,就是要用上标记永久化,据说二维线段树不支持打标记(也可能是我太菜了).
内层的线段树
struct SegmentSegmentTreeX//用两个结构体表示外传的线段树和内层的线段树,这样方便一点
{
int sgt[maxn<<2],tag[maxn<<2]/*用于记录标记*/;
void Updata(int now_left,int now_right,int cover,int now=1,int left=1,int right=M)//这里就是最普通的线段树了
{
if(now_right<left||right<now_left)return;
sgt[now]=max(sgt[now],cover);//修改当前子树
if(now_left<=left&&right<=now_right)
{
tag[now]=max(tag[now],cover);///修改标记
return;
}
Updata(now,cover,LEFT);
Updata(now,cover,RIGHT);
}
int Query(int now_left,int now_right,int now=1,int left=1,int right=M)//查询,实在没什么好说的
{
if(now_right<left||right<now_left)return -InF;
if(now_left<=left&&right<=now_right)
return max(sgt[now],tag[now]);
return max(tag[now],//要和标记取max
max(Query(now,LEFT),Query(now,RIGHT)));
}
};
外层的线段树
struct SegmentSegmentTreeY//外层线段树
{
SegmentSegmentTreeX sgt[maxn<<2],tag[maxn<<2];//这里的每个点和标记也是一颗线段树
void Updata(int nowlx,int nowly,int now_left,int now_right,int cover,int now=1,int left=1,int right=n)//其余基本相同,就是在原先的标记修改和子树修改需要改成修改内层的线段树
{
if(now_right<left||right<now_left)return;
sgt[now].Updata(nowlx,nowly,cover);
if(now_left<=left&&right<=now_right)
{
tag[now].Updata(nowlx,nowly,cover);
return;
}
Updata(nowX,cover,LEFT);
Updata(nowX,cover,RIGHT);
}
int Query(int nowlx,int nowly,int now_left,int now_right,int now=1,int left=1,int right=n)//查询也差不多
{
if(now_right<left||right<now_left)return -InF;
if(now_left<=left&&right<=now_right)
return sgt[now].Query(nowlx,nowly);
return max(tag[now].Query(nowlx,nowly),
max(Query(nowX,LEFT),Query(nowX,RIGHT)));
}
}SegmentSegmentTree;
扫描线
扫描线的作用&原理
放一道模板题.
给出n个矩形,求最终所覆盖的面积(矩阵面积并),看起来很像一道二维线段树,但是数据范围很大,是开不了的,所以,扫描线就诞生了.
图中的蓝色线段为扫面线.
先扫到最左边的矩形的左边的那条边:
可以发现在到下一条平行于扫描线的边之间是一个长方形,可以直接计算出来.
同样的方法可以将这个图形分成若干不相交的长方形.
每一块长方形的宽就是相邻的两条平行与扫面线的边之间的距离,长就是扫描线的长了,所以,问题就变成了如何计算扫面线的长,一个长方形有两条与扫描线平行的边,遇到左边的一条时就加上1,遇到右边时就减去1,这样扫面线的长度就是不为0的位置的个数,区间加减1很容易就想到线段树了.
扫描线的实现
#include<bits/stdc++.h>
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int maxn=2e5+7;
int n,M;
map<long long,long long>Hash;
long long place[maxn*2];
long long sor[maxn*2];
struct Line
{
int val;
long long x,fy,ly;
void into(int X,int F,int L,int V)
{
x=X;
fy=F;
ly=L;
val=V;
}
}line[maxn*2];
bool cmp(Line a,Line b)
{
return a.x<b.x;
}
struct SegmentTree
{
int cover;
int len;
}sgt[maxn*4];
#define LSON (now<<1)
#define RRSON (now<<1|1)
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RRSON,MIDDLE+1,right
#define NOW now_left,now_right
void PushUp(int now,int left,int right)
{
if(sgt[now].cover)//如果被完全覆盖,那么长度就是可以直接计算
{
sgt[now].len=place[right+1]-place[left];
}
else//没有被完全覆盖就合并儿子节点的信息
{
sgt[now].len=sgt[LSON].len+sgt[RRSON].len;
}
}
void Build(int now=1,int left=1,int right=n)//建树,没什么用
{
if(left==right)
{
sgt[now].cover=sgt[now].len=0;
return;
}
Build(LEFT);
Build(RIGHT);
PushUp(now,left,right);
}
void Updata(int now_left,int now_right,int add,int now=1,int left=1,int right=n)
{
if(now_right<=left||right+1/*注意需要加一,且用小于等于*/<=now_left)
{
return;
}
if(now_left<=left&&right+1/*注意加一*/<=now_right)
{
sgt[now].cover+=add;
PushUp(now,left,right);
return;
}
Updata(NOW,add,LEFT);
Updata(NOW,add,RIGHT);
PushUp(now,left,right);
}
int main()
{
scanf("%d",&M);
long long x1,y1,x2,y2;
REP(i,1,M)
{
scanf("%lld%lld%lld%lld",&x1,&y1,&x2,&y2);
sor[i*2-1]=y1;//数据太大,需要对于纵坐标离散
sor[i*2]=y2;
line[i*2-1].into(x1,y1,y2,1);
line[i*2].into(x2,y1,y2,-1);
}
sort(sor+1,sor+1+M*2);
sort(line+1,line+1+M*2,cmp);
sor[0]=114514233;
REP(i,1,M*2)
{
if(sor[i]!=sor[i-1])
{
Hash[sor[i]]=++n;
place[n]=sor[i];
}
}
Build();
long long answer=0;
REP(i,1,M*2-1)
{
Updata(Hash[line[i].fy],Hash[line[i].ly],line[i].val);//修改操作
answer+=sgt[1].len/*扫描线长*/*(line[i+1].x-line[i].x)/*两边之间的距离*/;
}
printf("%lld",answer);
}
主席树(可持久化权值线段树)
为什么叫主席树
主席树由一位名叫黄嘉泰的神仙最先用在静态区间 \(k\) 大中,然后可以惊奇的发现黄嘉泰的拼音首字母是hjt,然后...所以就叫主席树了.
主席树的作用&原理
在了解主席树之前先了解一下什么是持久化,类似有关访问历史版本的题目往往和持久化有关,如果需要一个可持久化的线段树需要怎么办呢,一个非常暴力的方法就是将所有历史版本的线段树都保存下来,但这样想必会MLE,且每次复制一个版本也需要 \(\mathcal{O}(n)\) 的复杂度并不可以.于是就出现了主席树这样一个神奇的数据结构.
如这样的一颗权值线段树,需要加如一个 \(3\).
可以发现只有红色部分的点的值改变了, 于是就发现每次的修改后有大量的点与之前相同,直接复制就浪费了大量空间,于是可以将新的产生的节点与没有改变的节点相连,这样就可以重复利用这些在历史版本中没有改变的值,节省大量空间,如这样的一个单点修改只会对一条链上的值产生影响,每一颗树就只要产生 \(\log_2n\) 个节点.
最终大概就是这个样子(完全不像树).
主席树的代码实现
单点修改
宏定义
//码风略微改变
#define LSON sgt[now].lson
#define RRSON sgt[now].rson
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RRSON,MIDDLE+1,right
#define NOW now_left,now_right
//新增加以下两句
#define NEW_LSON sgt[new_sgt].lson
#define NEW_RSON sgt[new_sgt].rson
修改
void Updata(int &new_sgt/*产生的一个新版本*/,int num/*num这个数在线段树中+1*/,int now,int left=1,int right=n)
{
if(num<left||right<num)//不会被修改到就直接连会原来的版本
{
new_sgt=now;
return;
}
new_sgt=++cnt;//一个新的节点
if(left==right)//叶节点就单点修改
{
sgt[new_sgt].sum=sgt[now].sum+1;//在原版本中+1得到新版本
return;
}
Updata(NEW_LSON,num,LEFT);//对于左右子树继续修改
Updata(NEW_RSON,num,RIGHT);
PushUp(new_sgt);//合并信息
}
对于主席树仅仅是针对可持久化权值线段树,所以主席树 \(\in\) 可持久化线段树.
区间修改可持久化线段树
区间修改主席树的作用&原理
例如在线段树1加上一个可持久化.可以发现如果像原来的可持久化线段树一样的方法去写每次修改最大就会产生一颗完整的线段树,肯定是会 MLE 的,所以就要拿出之前说过的标记永久化(其实不用标记永久化,直接打懒标记也是可以的).
对于一个修改的位置本来它的子孙都应该是被修改了,但是,加上了标记永久化以后就只需要修改自己,不用修改子孙了,所以对于完全被覆盖的点的左右儿子都还是原来的版本的值,注意在产生新版本的时候需要将原来的标记也赋值到新版本中.
区间修改主席树的实现
修改
void Updata(int now_left,int now_right,int add,int &new_sgt,int now,int left=1,int right=n)
{
if(now_left>right||left>now_right)
{
new_sgt=now;
return;
}
new_sgt=++cnt_point;
sgt[new_sgt].sum=sgt[now].sum+
1ll*add*(min(now_right,right)-max(now_left,left)+1);//当前修改对于当前区间的贡献
sgt[new_sgt].tag=sgt[now].tag;//赋值原来的标记
if(now_left<=left&&right<=now_right)//被完全覆盖
{
sgt[new_sgt].tag+=add;
NEW_LSON=LSON;//左右儿子不变
NEW_RSON=RRSON;
return;
}
Updata(NOW,add,NEW_LSON,LEFT);
Updata(NOW,add,NEW_RSON,RIGHT);
}
查询
long long Query(int now_left,int now_right,int now,int left=1,int right=n)
{
//与普通标记永久化线段树差不多
if(!now)
{
return 0;
}
if(now_left>right||left>now_right)
{
return 0;
}
if(now_left<=left&&right<=now_right)
{
return sgt[now].sum;
}
return Query(NOW,LEFT)+Query(NOW,RIGHT)+
1ll*sgt[now].tag*(min(now_right,right)-max(now_left,left)+1);//当前范围中的标记对于需要查询范围的贡献
}
带修主席树(其实就是树套树)
带修主席树的原理
先扔一道例题.
静态区间 \(k\) 大可以维护一个前缀每个数出现的次数,这样就可以用通过类似前缀和求区间和将这一个区间中每个数出现的次数计算出来,对于每个点开一颗线段树,因为有大量的相同点,所以可以用可持久化线段树.
再来看这道题,加上了一个单点修改的操作.可以再仔细想想,前缀和会想到什么呢....那就是树状数组啦,所以只要在树状数组上套一个线段树就好了.
代码实现
先建一下这个森林(一堆树)
修改
void Updata(int num,int val,int &new_sgt,int now,int left=1,int right=len)
//这里写主席树并没有什么用
{
if(num<left||right<num)
{
new_sgt=now;
return;
}
if(!new_sgt)
{
new_sgt=++point_cnt;//一个新节点
}
if(left==right)
{
sgt[new_sgt].sum=sgt[now].sum+val;
return;
}
Updata(num,val,nEW_LEFT,LEFT);
Updata(num,val,nEW_RIGHT,RIGHT);
PushUp(new_sgt);
}
void UpdataAdd(int top,int num,int val/*需要将原来的数减去,加上新的一个数*/)
{
for(int now=top;now<=n;now+=lowbit(now))//在树状数组上修改
{
Updata(num,val,root[now],root[now]);//在当前的树上修改
}
}
Build
void Build()
{
REP(i,1,n)
{
for(int now=i;now<=n;now+=lowbit(now))//一个点一个点地放入树状数组,记录前缀每个数数显次数
{
Updata(Hash[arr[i]],1,root[now],root[now]);
}
}
}
Query
int add_sgt[maxn];//记录在树状数组中需要加上的位置的树的当前根节点
int cut_sgt[maxn];//记录在树状数组中需要减去的位置的树的当前根节点
int Query(int k,int num_add,int num_cut,int left=1,int right=len)
{
if(left==right)return left;
int sum=0;
REP(i,1,num_add){sum+=sgt[sgt[add_sgt[i]].lson].sum;}//计算出所有左子树中的树的个数
REP(i,1,num_cut){sum-=sgt[sgt[cut_sgt[i]].lson].sum;}
if(sum>=k)
{
REP(i,1,num_add){add_sgt[i]=sgt[add_sgt[i]].lson;}//将根节点赋值为左儿子
REP(i,1,num_cut){cut_sgt[i]=sgt[cut_sgt[i]].lson;}
return Query(k,num_add,num_cut,left,MIDDLE);
}
REP(i,1,num_add){add_sgt[i]=sgt[add_sgt[i]].rson;}//同理赋值为右儿子
REP(i,1,num_cut){cut_sgt[i]=sgt[cut_sgt[i]].rson;}
return Query(k-sum,num_add,num_cut,MIDDLE+1,right);
}
int QueryKth(int left,int right,int k)
{
int num_add=0,num_cut=0;
for(int now=right;now;now-=lowbit(now))//将树状数组中需要加上的部分放入一个数组
{
add_sgt[++num_add]=root[now];
}
for(int now=left-1;now;now-=lowbit(now))//同理,将需要减去的部分放入一个数组
{
cut_sgt[++num_cut]=root[now];
}
return Query(k,num_add,num_cut);//查询kth
}
后记
文章中还有若干写的不清楚的地方,也有一些懒得写的地方,以后有空时可能会把锅补上.(发现问题可以私信我)
有关平衡树,多项式等内容,在若干年内也有可能会出类似学习笔记.
to be continued