【学习笔记】线段树详解(全)
和三个同学一起搞了接近两个月的线段树,头都要炸了T_T,趁心态尚未凉之前赶快把东西记下来。。。
【目录】
- 【基础】作者:((Silent)_(EAG))
- 【懒标记】作者:((Silent)_(EAG))
- 【扫描线】作者:((IC)_(QQQ))
- 【权值线段树】作者:((Xing)_(Ling))
- 【动态开点】作者:((Xing)_(Ling))
- 【线段树合并】作者:((Xing)_(Ling))
- 【可持续化线段树—静态主席树】作者:((Xing)_(Ling))
- 【可持续化线段树—动态主席树(
岂可修)】作者:((Xing)_(Ling))
一:【基础姿势】
(piao) 一下隔壁大佬的文章QAQ: (Silent)_(EAG)
基础题走起:
-
【模板】线段树 (1) ([P3372])
【标签】线段树/树状数组
【题解】(Silent)_(EAG) -
(Can) (you) (answer) (these) (queries) (I) ([SP1043])
【标签】线段树 -
(Interval) (GCD) ([CH4302])
【标签】线段树/树状数组/(GCD)
【题解】(Silent)_(EAG)
高档题走起:
-
(Can) (you) (answer) (these) (queries) (III) ([SP1716])
【标签】线段树/懒标记
【题解】(Silent)_(EAG) -
炸勃龙 (I) ([P4118]) ([BZOJ5394]) (/) 奈芙莲·卢可·印萨尼亚 (Nephren) (Ruq) (Insania) ([P3934])
【标签】线段树/树状数组/暴力枚举/数论/欧拉定理
((YNOI2016) 毒瘤数论)
二:【懒标记】
见隔壁:(Silent)_(EAG)
基础题走起:
- 【模板】线段树 (2) ([P3373]) (/) 维护序列 ([P2023]) ([BZOJ1798])
【标签】线段树/懒标记
【题解】(Silent)_(EAG)
高档题走起:
-
(Can) (you) (answer) (these) (queries) (IIII) ([SP2713])
【标签】线段树/懒标记
【题解】(Silent)_(EAG) -
市场 ([LOJ6029])
【标签】线段树/懒标记
【题解】(Silent)_(EAG) -
酒店 (Hotel) ([P2894]) ([BZOJ1593])
【标签】线段树/懒标记 -
序列操作 ([P2572]) ([BZOJ1858])
【标签】线段树/懒标记/珂朵莉树
(如果尝试用线段树做的话,懒标记的运用达到了毁天灭地的地步。而珂学就爽了,那么它就是道大水题)
三:【扫~描~线~】
见隔壁:(IC)_(QQQ)
四:【权值线段树】
QAQ:以前似乎在某个地方看到过一个叫做做权值树状数组的东西唉QWQ
DL:其实他们都是一样的思想,只是不同的实现方式而已
QAQ:可是XX在写过无数区间作业的题后,发现无论是哪一类,树状数组都比线段树快不止三倍唉QWQ
DL:*****
权值线段树是什么?
权值线段树,顾名思义是一颗线段树。
但它和普通线段树略有不同: 普通线段树维护的是一段区间的数值的总和或最大值等等...... 而权值线段树维护的是一定范围内某个数值出现的次数。实际上它和之前的权值树状数组是一样的原理。(求逆序对也可用权值线段树实现 )
它有什么用?
按照定义,我们可以用它对于权值进行计数,感觉有点像数位(dp)吧,求一定范围内符合要求的数的个数。权值线段树的(“)范围(”)是不定的,而(“)要求(”)一般是:在给定的数值范围内。
举个栗子:
有一个数列: (a(1,1,2,3,3,4,4,4,4,5)) 对其维护一个计数的权值线段树,树的大小就是数列的值域 ([mins~maxs]),即 ([1~5])。
如图,在数列中:
数值 (1) 出现了 (2) 次
数值 (2) 出现了 (1) 次
数值 (3) 出现了 (2) 次
数值 (4) 出现了 (4) 次
数值 (5) 出现了 (1) 次
重要应用:
在此基础上,它还有一个很重要的作用 :
查询某区间内第 (k) 小或第 (k) 大的值。
引理:
如果在值域范围内 (() 即 ([mins~maxs]) ())中发现有某个位置 (x),数列中存在这个数 (x),且使得数值范围在区间 ([mins~x]) 内的数一共有 (k) 个,那么 (x) 就是第 (k) 小的数。
反之亦然:
如果在值域范围内 (() 即 ([mins~maxs]) ())中发现有某个位置 (x),数列中存在这个数 (x),且使得数值范围在区间 ([x~maxs]) 内的数一共有 (k) 个,那么 (x) 就是第 (k) 大的数。
【分析】
以查询第 (k) 小为例
我们可以用一种二分的思想,当需要在某个值域范围 ([l~r]) 内查找第 (k) 小时,先计算出数值在 ([l~mid]) 以内的数的个数 (tmp),再将其与 (k) 进行比较:
- 如果 (tmp geqslant k),则说明第 (k) 小的数应存在于 ([l~mid]) 这个范围。
- 如果 (tmp leqslant k),则说明第 (k) 小的数应存在于 ([mid+1~r]) 这个范围,而实际上就等价于在 ([mid+1~r]) 中找到第 (k-tmp) 小的数。
【Code】
#define Re register int
#define pl tree[p].PL
#define pr tree[p].PR
inline int ask(Re p,Re L,Re R,Re k){//查询第k小
if(L==R)return L;//边界叶节点
Re tmp=tree[pl].g;//计算左子树(数值范围在L~mid的数)共有多少个数字
if(tmp>=k)return ask(pl,L,mid,k);
//左子树已经超过k个,说明第k小在左子树里面
else return ask(pr,mid+1,R,k-tmp);
//左子树不足k个数字,应该在右子树中找到第(k-tmp)小
}
五:【动态开点】
什么是动态开点?
动态开点用法较固定,目的也很明确:节省空间。
它的实质其实就是在空间不够的情况下,把不需要的节点变成虚点。
有什么用?
求解逆序对时可以用权值树状数组,那么如果尝试用权值线段树做的话会出现什么后果呢?
肯定是可解的。但是,由于值域大多都是 (inf) 级别的数字,况且某些比较毒瘤的在线操作还没法离散化,于是在使用权值线段树时,一般都会伴随着动态开点的使用。
如何使用?
这里引用一下一位大佬的比喻: 开局一个根,枝叶全靠给。
当要用到(一般只有修改)某个节点的信息时,就手动开一个新的节点,给它一个点的空间包括各种节点信息。而在查询中如果发现进入的节点不存在(还没开发过),那么直接返回,不需要在查询时新建节点。
【空间复杂度】
(Q*log(inf))。其中 (Q) 为修改次数。
【Code】
(基本框架)
int cnt;
inline void sakura(Re &p,Re L,Re R,Re ???){//【???修改】
if(!p)p=++cnt,tree[p].?=???;
//发现进入了一个空节点,新建一个节点,赋予它编号,记录基本信息
if(L==R){tree[p].?=???;return;}
//达到叶子节点,记录一些特殊的信息,并返回
Re tmp=???;//可能会在在递归之前进行一些计算来方便判断
if(???)sakura(pl,L,mid,???);//递归进入左子树
if(???)sakura(pr,mid+1,R,???);//递归进入右子树
tree[p].?=???;//回溯后更新信息
}
六:【线段树合并】
什么是线段树合并?
简单来说就是将两棵线段树合并起来,并累加它们的信息。
有什么用?
线段树合并一般用于对树上信息的统计,例如:对一棵树的所有叶子节点都开一个线段树,统计信息时,将所有的儿子节点的线段树合并起来,得到父亲节点的线段树,再用其去合并统计祖先的信息。
【时间复杂度】
如果一棵线段树的所有节点都不为空(动态开点会使得虚点的存在),离散化后值域为 (n),递归一棵线段树树的时间复杂度达到最大: (O(logn))。如果总共 (n) 棵树的所有节点都不为空,那么需要合并 (n-1) 次, 总时间复杂度达到最大: (O(n*logn))。
【Code】
inline int merge(Re p,Re q){//【线段树合并】
if(!p)return q;if(!q)return p;
//当需要合并的点的其中一个编号为0时 (即为空),返回另一个编号
tr[p].g+=tr[q].g,p;//把q合并到p上面去
pl=merge(pl,tr[q].lp);//合并左子树,并记录p点的左子树编号
pr=merge(pr,tr[q].rp);//合并右子树,并记录p点的右子树编号
return p;
}
基础题走起:
-
晋升者计数 (Promotion) (Counting) ([P3605])
【标签】DFS/离散化/树状数组/线段树/线段树合并/动态开点
【题解】(Xing)_(Ling) -
永无乡 ([P3224]) ([BZOJ2733])
【标签】Splay/平衡树/线段树合并/DFS/权值线段树/并查集/离散化
【题解】(Xing)_(Ling)
高档题走起:
- (ROT-Tree) (Rotations) ([P3521]) ([BZOJ2212])
【标签】线段树/权值线段树/线段树合并
【题解】(IC)_(QQQ)
七:【可持续化线段树—静态主席树】
来看一道经典的例题 【模板】可持久化线段树 (1) (主席树) ([3834]) (/) (K)-(th) (Number) ([P3834]) ([POJ2104]) ([SP3946])
【题目大意】
给定一个长为 (n) 的数列以及 (Q) 个查询,每次查询输入两个整数 (l),(r),输出数列中 (l) ~ (r) 第 (K) 小的数。
【 分析】
此题有三个关键点:
-
求一棵数列中的第 (K) 大或第 (K) 小的数。
解决方案:权值线段树 -
如果仅仅是这样,则非常好办,给出的询问是一段区间。一段长为 (n) 的数列中共有 (n(n-1)/2) 个不同的区间,如果给每个区间都开一个权值线段树,后果是:(TLE) (+) (MLE) (+) 初始化建树无从入手。
解决方案:前缀和
(对于原数列的每个位置都建立一个权值线段树,(p[i]) 表示第 (i) 个位置上的树的根节点编号,用 (tree[pt[i]]) 表示从第 (1) 个到第 (i) 个数这个区间中共 (i) 个数所维护成的一棵权值线段树。(message[pt[i]]=sum_{j=1}^i message[pt[j]])) -
可内存还是远远不够,即使是使用了【动态开点】 (+) 离散化,值域由 (inf) 降为 (N),每一棵权值线段树的节点数降为 (N*2),节点,但一共有 (N) 棵树,(N*N*2) 动辄就是几十万兆内存。(做一个简单的计算:
(200000*200000*2*4*3/1024/1024 hickapprox 305175.78125 Mb))
解决方案:可持续化
由于第 (i) 棵树 (tree[pt[i]]) 与第 (i-1) 棵树 (tree[pt[i-1]]) 只有 (logn) 个节点不一样,于是只需要对第 (i-1) 棵树进行一次单点修改将 (a[i]) 加入,就变成了第 (i) 棵树。因此我们可以由已经建好第 (i-1) 棵树迅速建立起第 (i) 棵树,这也就是主席树思想的精髓所在。
说具体点,就是让第 (i) 棵树与第 (i-1) 棵树公用一些节点(因为在这些没有发生改变的部分,它们的信息是完全相同的),在递归过程建树中,如果发现要进行【单点修改】操作的是左子树,那就让新树的右子树编号指向旧树的右子树编号(即 (tree[pt[i]].pr=tree[pt[i-1]].pr) )然后递归进入左子树的建立,反之亦然。
但第 (1) 棵树需要由第 (0) 棵树变化而来,当有特殊需要时,要提起把第 (0) 棵树建立完整,而道题不需要,因为第 (0) 棵树本来就为空,所以不用管。
【时间复杂度】
每次建树的过程都接近于【单点修改】, 为 (O(logn)) ,所以初始化建树的过程为 (O(nlogn)) 。
单次询问采用权值线段树中的【查询第 (k) 小】,为 (O(logn))。
总共 (Q) 次询问,为 (O(Qlogn)) 。
总时间复杂度: (O((n+Q)logn))
【空间复杂度】
如果要建立完整的第 (0) 棵树,会占用 (n*2) 个节点,每棵新树的建立都要新建 (logn) 个节点,一共有 (nlogn)。
总空间复杂度:((logn+2)*n)
【Code】
#include<algorithm>
#include<cstdio>
#define mid (L+R>>1)
#define pl tr[p].lp
#define pr tr[p].rp
#define Re register int
#define F(a,b) for(i=a;i<=b;++i)
using namespace std;
const int N=1e5+3;
int x,y,z,i,n,m,k,t,fu,cnt,a[N],b[N],pt[N];//pt[i]表示离散化后i这个位置所对应的权值树根的编号
struct QAQ{int g,lp,rp;}tr[N<<5];//权值树,保守开一个32*N
inline void in(Re &x){//【快读】自己动手,丰衣足食...
x=fu=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();x=fu?-x:x;
}
inline int creat(Re pp,Re x,Re L,Re R){
//把上一棵权值树pp(即pt[a[i-1]])复制过来
//并在递归复制途中对x(即a[i]离散化后的位置)这个点同步进行单修操作
Re p=++cnt;pl=tr[pp].lp,pr=tr[pp].rp,tr[p].g=tr[pp].g+1;
//新开一个点,并把上一个的数据复制进来,并使tr[].g++
if(L==R)return p;//到达边界: L==R(即x这个位置)
if(x<=mid)pl=creat(tr[pp].lp,x,L,mid);//递归进入条件:单修
else pr=creat(tr[pp].rp,x,mid+1,R);//注意tr[pp]要同时递归至左(右)子树
return p;
}
inline int ask(Re p,Re pp,Re L,Re R,Re k){
//查询。p为查询区间左端点的权值树根编号,pp为查询区间右端点的权值树根编号
if(L==R)return b[R];//边界:L==R
Re tmp=tr[tr[pp].lp].g-tr[pl].g;//用前缀和思想计算出左子树共有多少个数字
if(tmp>=k)return ask(pl,tr[pp].lp,L,mid,k);//左子树已经超过k个,说明第k小在左子树里面
else return ask(pr,tr[pp].rp,mid+1,R,k-tmp);//左子树不足k个,应该在右子树中找第(k-tmp)小
}
int main(){
in(n),in(k);
F(1,n)in(a[i]),b[i]=a[i];//复制进b[]并离散去重
sort(b+1,b+n+1);//【离散化】
m=unique(b+1,b+n+1)-b-1;//【去重】
F(1,n)pt[i]=creat(pt[i-1],lower_bound(b+1,b+m+1,a[i])-b,1,m);
//找出当前这个位置按权值排序后的位置x,进入建树
while(k--)in(x),in(y),in(z),printf("%d
",ask(pt[x-1],pt[y],1,m,z));//注意是【y】-【x-1】
}
基础题走起:
-
【模板】可持久化数组(可持久化线段树/平衡树) ([P3919])
【标签】平衡树/线段树/主席树/可持续化 -
(KUR-Couriers) ([P3567])
【标签】线段树/主席树/可持续化
八:【可持续化线段树—动态主席树(岂可修)】
一样的,先上例题: (Dynamic) (Rankings) ([P2617]) ([ZOJ2112]) ([BZOJ1901])
【题目大意】
在【静态主席树】的基础上加了一个【单点修改】的操作。
【分析】
回想一下,在刚刚静态线段树中用到了前缀和的思想:对于原数列的每个位置都建立一个权值线段树,用 (tree[pt[i]]) 表示从第 (1) 个到第 (i) 个数这个区间中共 (i) 个数所维护成的一棵权值线段树。
它维护的是每个位置前面的一整个大区间,而如果对某个位置 (i) 上的数进行了修改,那么从 (i) 这个位置开始以后所有的位置上存的信息都要改变(因为位置 (i) 后面的所有位置上所存的信息都包含了 (i) 的信息)。 那么一次【单点修改】的时间复杂度就是接近于 (O(nlogn)) 的,显然不能吗,满足要求。
解决方案:树(树状数组)套树
【树状数组】每一个位置 (i) 上的数据 (C[i]) 维护的信息是: (message[i]=sum_{j=i-lowbit(i)+1}^i message[j])
而【静态主席树】每一个位置 (tree[pt[i]]) 上的数据 (C[i]) 维护的是: (message[pt[i]]=sum_{j=1}^i message[pt[j]])
【树状数组】:查询 (O(logn)),修改 (O(logn))
【静态主席树 (+) 暴力】:查询 (O(logn)),修改 (O(nlogn))
实际上静态主席树慢就慢在维护的前缀和范围实在是太大了,尤其是最后一个位置,直接就是 ([1) ~ (N]) 的范围。既然如此,为什么不可以把范围缩小一点呢?如果让其维护和树状数组一样的区间,虽然查询第 (k) 小上面加了一个 (O(logn)),可【单点修改】却降下了一个 (O(logn)) ,于是整个问题的时间复杂度就降了下来,可以完美解决了。
【时间复杂度】
每次查询递归 (logn) 层,每层最多需要对 (max(lowbit(i)) hickapprox logn) 个节点进行修改,为 (O(log^2n))。
同理,【单点修改】也应该是 (O(log^2n))。
一共 (Q) 次操作,为 (O(Qlog^2n))
初始化建树是 (n) 次【单点修改】,为 (nlog^2n)
总时间复杂度为:(O((n+Q)log^2n))
【空间复杂度】
有 (Q) 次操作,如果每次操作都是【单点修改】且每次都是改的一个与之前都不同的数,那么离散化的值域为 (Q+n)。
如果要建立完整的第 (0) 棵树,会占用 (n*2) 个节点,每棵新树的建立都要新建 (log^2(Q+n)) 个节点,一共有 (nlog^2(Q+n))。
(Q) 次操作,有 (Qlog^2(Q+n))
总空间复杂度:((Q+n)*log^2(Q+n))
数据规模:
(1 leqslant N,Q leqslant 1e5)
(200000*21*21*4*3/1024/1024)
算下来大概是 (1009MB),而跑下来实际是大约 (154.14Mb)
所以不用慌张,因为实际空间一定是远远小于理论空间的,出题人再怎么良心也不会故意弄一个卡极限的 (1000Mb) 数据出来,况且这一类毒瘤题空间限制都大得惊人。
这是洛谷的时空限制:
【Code】
#include<algorithm>
#include<cstring>
#include<cstdio>
#define mid (L+R>>1)
#define Re register int
#define F(i,a,b) for(Re i=a;i<=b;++i)
using namespace std;
const int N=2e5+3;char opt[N];
int x,y,z,n,m,T,t,fu,cnt,tl,tr,a[N],b[N],pt[N],C[N],ptl[20],ptr[20];
//ptl,ptr千万不要开N,否则memset的时候会TLE到怀疑人生
struct QAQ{int g,lp,rp;}tree[N*400];//本应是441左右,开小一点也无所谓,因为根本用不到
struct O_O{int l,r,k;}Q[N];//储存Q次查询的内容,方便离散化
struct T_T{int i,x;}c[N];//离散化数组
inline void in(int &x){
x=fu=0;char c=getchar();
while(c<'0'||c>'9')fu|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=fu?-x:x;
}
inline int ask_(Re L,Re R,Re k){
if(L==R)return b[R];//注意:返回的值需要用到的是哪一个映射数组不能搞错
Re tmp=0;
F(i,1,tl)tmp-=tree[tree[ptl[i]].lp].g;//计算左子树信息
F(i,1,tr)tmp+=tree[tree[ptr[i]].lp].g;//计算左子树信息
if(tmp>=k){
F(i,1,tl)ptl[i]=tree[ptl[i]].lp;//更新ptl,ptr所指向的节点编号
F(i,1,tr)ptr[i]=tree[ptr[i]].lp;
return ask_(L,mid,k);
}
else{
F(i,1,tl)ptl[i]=tree[ptl[i]].rp;
F(i,1,tr)ptr[i]=tree[ptr[i]].rp;
return ask_(mid+1,R,k-tmp);
}
}
inline int ask(Re L,Re R,Re k){//查询第k小
memset(ptl,0,sizeof(ptl));//万恶的memset
memset(ptr,0,sizeof(ptr));//数组开太大会疯狂抢时间复杂度
tl=tr=0;
for(Re i=L-1;i;i-=i&-i)ptl[++tl]=pt[i];//先把所有要更新的位置的线段树根节点记录下来
for(Re i=R;i;i-=i&-i)ptr[++tr]=pt[i];//方便后面递归更新信息
return ask_(1,m,k);
}
inline void change(Re &p,Re L,Re R,Re w,Re v){
if(!p)p=++cnt;tree[p].g+=v;
if(L==R)return;
if(w<=mid)change(tree[p].lp,L,mid,w,v);
else change(tree[p].rp,mid+1,R,w,v);
}
inline void add(Re x,Re v){//【单点修改】
Re w=lower_bound(b+1,b+m+1,a[x])-b;//注意函数传进来的参数和这里各种映射数组的调用不要搞错
for(Re i=x;i<=n;i+=i&-i)change(pt[i],1,m,w,v);//树状数组思想更新信息
}
int main(){
// printf("%lf
",(sizeof(tree))/1024.0/1024.0);
// printf("%lf
",(sizeof(tree)+sizeof(Q)+sizeof(c)+sizeof(a)+sizeof(b)+sizeof(pt)+sizeof(C))/1024.0/1024.0);
in(n),in(T),m=n;
F(i,1,n)in(a[i]),b[i]=a[i];
F(i,1,T){
scanf(" %c",&opt[i]);
if(opt[i]=='Q')in(Q[i].l),in(Q[i].r),in(Q[i].k);
else in(c[i].i),in(c[i].x),b[++m]=c[i].x;
}
sort(b+1,b+m+1);
m=unique(b+1,b+m+1)-b-1;
F(i,1,n)add(i,1);//初始化建树
F(i,1,T){
if(opt[i]=='Q')printf("%d
",ask(Q[i].l,Q[i].r,Q[i].k));
else add(c[i].i,-1),a[c[i].i]=c[i].x,add(c[i].i,1);
//先让这个位置上原来的数减少一个,再把新数加一个,就达到了替换的目的
}
}
高档题走起:
- 【模板】二逼平衡树(树套树)([P3380]) ([BZOJ3196]) ([TYVJ1730])
【标签】树套树/平衡树/线段树/树状数组
【题解】(Xing)_(Ling)
(略微有点变态)