线段树是一种可以处理区间问题的优秀数据结构.
线段树是一颗二叉树, 其中的每一个节点都代表了某个区间的信息.
普通线段树
这里默认您已经会了以下操作:
- 建树(以单点修改的形式)
- 单点修改/查询
- 区间查询
如果不会的话请见OI Wiki
着重讲解区间修改中 tag
的用法
对于区间修改, 如果仍然使用单点修改的方式, 肯定会 TLE. 因此使用 tag
进行暂存操作
用于更新/与原来不同的函数: pushdown 函数, update 函数, query 函数
Pushdown
对于当前的节点
x
x
x , 把它身上的 tag
推给他的儿子, 并且直接给它的两个儿子加上区间增加的和
void pushdown(int x,int l,int r){
if(tag[x]!=0){
tag[x<<1]+=tag[x];
tag[x<<1|1]+=tag[x];
int mid=(l+r)>>1;
t[x<<1]+=(mid-l+1)*tag[x];
t[x<<1|1]+=(r-mid)*tag[x];
tag[x]=0;
}
}
Update
对于当前节点
x
x
x , 我们先要判断当前节点代表区间在不在修改区间范围内, 如果是, 修改当前 tag
值, 并直接对当前区间加上区间增加和.
如果不是, 就要把自己的 tag
值推给两个儿子(因为当前这个点所包含的区间无法完全囊括要修改的区间), 之后进行二分, 看往左/右继续更新
void update(int x,int l,int r,int L,int R,int val){
if(l>=L && r<=R){
tag[x]+=val;
t[x]+=(r-l+1)*val;
return;
}
int mid=(l+r)>>1;
pushdown(x,l,r);
if(mid>=L){
update(x<<1,l,mid,L,R,val);
}
if(mid<R){
update(x<<1|1,mid+1,r,L,R,val);
}
t[x]=t[x<<1]+t[x<<1|1];
}
Query
与 Update 类似, 对于当前节点 x x x ,如果它再查询区间内, 直接返回该该点值, 要么就向下更新, 继续查询
int query(int x,int l,int r,int L,int R){
if(l>=L && r<=R){
return t[x];
}
pushdown(x,l,r);
int mid=(l+r)>>1,ans=0;
if(mid>=L){
ans+=query(x<<1,l,mid,L,R);
}
if(mid<R){
ans+=query(x<<1|1,mid+1,r,L,R);
}
return ans;
}
最后把这些步骤整合到一起, 就形成了可以进行完整区间操作的线段树
Scode
#include <bits/stdc++.h>
using namespace std;
namespace Radon{
void Main();
}
int main(){
Radon::Main();
return 0;
}
namespace Radon{
#define N 1000102
#define int long long
int n,m;
int t[N<<2],tag[N<<2];
void pushdown(int x,int l,int r){
if(tag[x]!=0){
tag[x<<1]+=tag[x];
tag[x<<1|1]+=tag[x];
int mid=(l+r)>>1;
t[x<<1]+=(mid-l+1)*tag[x];
t[x<<1|1]+=(r-mid)*tag[x];
tag[x]=0;
}
}
void build(int x,int l,int r,int num,int val){
if(l==r){
t[x]=val;
return;
}
int mid=(l+r)>>1;
if(mid>=num){
build(x<<1,l,mid,num,val);
}else{
build(x<<1|1,mid+1,r,num,val);
}
t[x]=t[x<<1]+t[x<<1|1];
}
void update(int x,int l,int r,int L,int R,int val){
if(l>=L && r<=R){
tag[x]+=val;
t[x]+=(r-l+1)*val;
return;
}
int mid=(l+r)>>1;
pushdown(x,l,r);
if(mid>=L){
update(x<<1,l,mid,L,R,val);
}
if(mid<R){
update(x<<1|1,mid+1,r,L,R,val);
}
t[x]=t[x<<1]+t[x<<1|1];
}
int query(int x,int l,int r,int L,int R){
if(l>=L && r<=R){
return t[x];
}
pushdown(x,l,r);
int mid=(l+r)>>1,ans=0;
if(mid>=L){
ans+=query(x<<1,l,mid,L,R);
}
if(mid<R){
ans+=query(x<<1|1,mid+1,r,L,R);
}
return ans;
}
void Main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin >> n >> m;
for(int x=1;x<=n;x++){
int a;
cin >> a;
build(1,1,n,x,a);
}
for(int x=1;x<=m;x++){
int opt,a,b;
cin >> opt >> a >> b;
if(opt==1){
int w;
cin >> w;
update(1,1,n,a,b,w);
}else{
cout << query(1,1,n,a,b) << endl;
}
}
}
}
动态开点线段树
动态开点, 与 vector
的原理差不多, 都是用到哪些点, 就使用哪些点.
所以线段树会发生以下改变 :
-
再建树的时候, 不在使用 x < < 1 x<<1 x<<1 , x < < 1 ∣ 1 x<<1|1 x<<1∣1 代表 l s o n lson lson 或 r s o n rson rson , 而是使用以下操作:
void build(int &x,int l,int r,int num,int val){ if(x==0){ x=++cnt; } if(l==r){ t[x]=val; return; } int mid=(l+r)>>1; if(num<=mid){ build(ls[x],l,mid,num,val); }else{ build(rs[x],mid+1,r,num,val); } t[x]=t[ls[x]]+t[rs[x]]; }
其中, c n t cnt cnt 表示已经使用的节点数量(也可以认为是要使用的下一个节点的编号), 而使用
int &x
可以做到直接更改ls[x]
和rs[x]
代表的下标 -
对于一个节点 x x x , 他的左右儿子不再是 x < < 1 x<<1 x<<1 , x < < 1 ∣ 1 x<<1|1 x<<1∣1, 而是变成了某个编号. (用 l s o n lson lson 和 r s o n rson rson 记录)
-
其他的地方与正常的线段树完全相同, 只需要改变左右儿子的表达方式即可
Scode
#include <bits/stdc++.h>
using namespace std;
namespace Radon{
void Main();
}
int main(){
Radon::Main();
return 0;
}
namespace Radon{
#define int long long
#define N 100010*2
int n,m;
int ls[N],rs[N];
int tag[N];
int t[N];
int cnt=1;//这里初值要赋为1的原因是,我们默认表示区间1~n的节点是存在的,不需要额外去新建
void pushdown(int x,int l,int r){
if(tag[x]!=0){
if(ls[x]==0){
ls[x]=++cnt;
}
if(rs[x]==0){
rs[x]=++cnt;
}
tag[ls[x]]+=tag[x];
tag[rs[x]]+=tag[x];
int mid=(l+r)>>1;
t[ls[x]]+=(mid-l+1)*tag[x];
t[rs[x]]+=(r-mid)*tag[x];
tag[x]=0;
}
}
void build(int &x,int l,int r,int num,int val){
if(x==0){
x=++cnt;
}
if(l==r){
t[x]=val;
return;
}
int mid=(l+r)>>1;
if(num<=mid){
build(ls[x],l,mid,num,val);
}else{
build(rs[x],mid+1,r,num,val);
}
t[x]=t[ls[x]]+t[rs[x]];
}
void update(int &x,int l,int r,int L,int R,int val){
if(x==0){
x=++cnt;
}
if(l>=L && r<=R){
tag[x]+=val;
t[x]+=(r-l+1)*val;
return;
}
int mid=(l+r)>>1;
pushdown(x,l,r);
if(L<=mid){
update(ls[x],l,mid,L,R,val);
}
if(R>mid){
update(rs[x],mid+1,r,L,R,val);
}
t[x]=t[ls[x]]+t[rs[x]];
}
int query(int x,int l,int r,int L,int R){
if(x==0){
return 0;
}
if(l>=L && r<=R){
return t[x];
}
pushdown(x,l,r);
int ans=0,mid=(l+r)>>1;
if(mid>=L){
ans+=query(ls[x],l,mid,L,R);
}
if(mid<R){
ans+=query(rs[x],mid+1,r,L,R);
}
return ans;
}
void Main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin >> n >> m;
for(int x=1;x<=n;x++){
int a;
cin >> a;
int num=1;
build(num,1,n,x,a);
}
for(int x=1;x<=m;x++){
int opt,a,b;
cin >> opt >> a >> b;
if(opt==1){
int qw;
cin >> qw;
int num=1;
update(num,1,n,a,b,qw);
}else{
cout << query(1,1,n,a,b) << endl;
}
}
}
}
权值线段树
注 : 普通权值线段树只能做到对某个固定区间求某些数的出现个数, 如果要求动态区间, 请见可持久化线段树
权值线段树可以统计固定区间内总数的出现个数
这时候每个节点的含义就变为了它所代表的区间为值域, 每个数出现次数的总和
eg : 若有一个区间 { 1 , 5 , 2 , 3 , 4 , 1 , 3 , 4 , 4 , 4 } \{ 1,5,2,3,4,1,3,4,4,4 \} {1,5,2,3,4,1,3,4,4,4}, 那么它在权值线段树里长这样 :
次图片来源于添加链接描述
其中, 最下边的叶子节点的值代表某个值出现的次数
容易发现, 权值线段树的大小与值域有关, 如果 r ≤ 1 0 18 r\le10^{18} r≤1018 就会爆掉, 因此使用动态开点
具体实现 :
Update(Build)
使用 update 进行建树操作, 本质就是对于某个节点下标 +1
void update(int &x,int l,int r,int num,int val){
if(x==0){
x=++cnt;
}
// cout << x << ':' << l << ' ' << r << ' ' << num << endl;
if(l==r){
t[x]+=val;
return;
}
int mid=(l+r)>>1;
if(mid>=num){
update(ls[x],l,mid,num,val);
}else{
update(rs[x],mid+1,r,num,val);
}
t[x]=t[ls[x]]+t[rs[x]];
}
Query
正常按照线段树查询即可
例题 : P1908 逆序对
把每个数依次加到线段树中, 并且查询当前比他大的数有多少即可
Scode
#include <bits/stdc++.h>
using namespace std;
namespace Radon{
void Main();
}
int main(){
Radon::Main();
return 0;
}
namespace Radon{
#define LL long long
//十点OI一场空,__ __ __ __ __ __ __
#define INF 1000000010
#define N 10000010
int n;
LL a[N];
int t[N],ls[N],rs[N];
LL cnt=1;
void update(int &x,int l,int r,int num,int val){
if(x==0){
x=++cnt;
}
// cout << x << ':' << l << ' ' << r << ' ' << num << endl;
if(l==r){
t[x]+=val;
return;
}
int mid=(l+r)>>1;
if(mid>=num){
update(ls[x],l,mid,num,val);
}else{
update(rs[x],mid+1,r,num,val);
}
t[x]=t[ls[x]]+t[rs[x]];
}
LL query(int x,int l,int r,int L,int R){
if(x==0){
return 0;
}
if(l>=L && r<=R){
return t[x];
}
int ans=0,mid=(l+r)>>1;
if(mid>=L){
ans+=query(ls[x],l,mid,L,R);
}
if(mid<R){
ans+=query(rs[x],mid+1,r,L,R);
}
return ans;
}
void Main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
LL ans=0;
cin >> n;
for(int x=1;x<=n;x++){
cin >> a[x];
int temp=1;
update(temp,1,INF,a[x],1);
ans+=query(1,1,INF,a[x]+1,INF-5);
// cout << a[x] << ':' << query(1,1,INF,a[x]+1,INF);
}
cout << ans;
}
}
可持久化线段树(主席树)
单点修改, 区间查询
主席树必须使用动态开点
可持久化, 顾名思义, 就是可以保存之前某个状态的一种方式.
由于单点修改的性质, 每次修改只会更改一条链上的值(如下图, 如果我要修改 8 8 8 节点, 只会对红色编号的节点修改).
因此如果我想查询过去某个版本线段树上的信息, 直接对每个版本都建立线段树会 TLE+MLE, 因此我们可以尝试每次在原树 (或者说实在哪个版本上修改, 就以哪个版本为原树) 新建一条链即可. 这时候就相当于又有了一颗线段树, 需要储存其根节点的编号
如下图 :
白色的 1 ∼ 15 1\sim15 1∼15 节点为线段树的版本一(可以视为初始状态)
这时候如果我要令 a [ 1 ] a[1] a[1] ++(即白色 8 8 8 号节点 ++), 正常流程是修改白色 1 , 2 , 4 , 8 1,2,4,8 1,2,4,8 节点权值, 但是这样做就会我们失去版本一 (原树) 的部分信息. 因此我们直接新建一条红色的链, 链里存放的是修改完后的状态
红色的 1 , 2 , 4 , 8 1,2,4,8 1,2,4,8 和 白色的剩余节点(不包括白色的 1 , 2 , 4 , 8 1,2,4,8 1,2,4,8 )为线段树的版本二
我们把白色的 1 1 1 认作是版本一线段树的根, 而红色的 1 1 1 被认为是版本二的根
对于这条链上的每一个节点 (如红 1 1 1 ) , 可以发现它和它的母体节点 (如白 $1 $ ) 有相似之处 : 新节点的权值可以由母体节点转移来, 新节点的左右儿子部分与母体节点相同.
因此我们需要按顺序进行以下操作, 来完成克隆一份母体节点, 作为新节点的初始值:
-
把母体节点的信息完全复制一份
如红 1 1 1 节点需要复制白 1 1 1 的全部信息, 包括左右儿子编号, 节点内存储的值
-
将当前节点的编号更改, 变成一个新的点(与动态开点密切相关)
-
根据题中条件向左/右儿子继续递归新建
可以使用
&ls[x]
的方式直接改变左右儿子的编号值(利用递归到左右儿子时进行的操作 2 2 2 )
如果知道了过去查询的版本号, 直接从那个版本的根开始查询即可
现在考虑如何操作 :
void build(int &x,int l,int r,int num,int val){
if(x==0){x=++cnt;}
if(l==r){t[x]=val;return;}
int mid=(l+r)>>1;
if(num<=mid){ build(ls[x],l,mid,num,val); }
if(mid<r){ build(rs[x],mid+1,r,num,val); }
t[x]=t[ls[x]]+t[rs[x]];//整合信息, 不一定是加
}
Build
对于简述操作, 和动态开点线段树完全相同. 但是某些时候, 建树操作可以被更新(插入)操作代替. 如 P3834 【模板】可持久化线段树 2
int clone(int x){
t[++cnt]=t[x]; ls[cnt]=ls[x]; rs[cnt]=rs[x];
return cnt;
}
Clone
直接把目标节点的信息复制到新节点中, 并且传回新节点编号 c n t cnt cnt
void insert(int &x,int l,int r,int val){
x=clone(x);
if(l==r){t[x]++;return;}
int mid=(l+r)>>1;
if(val<=mid){insert(ls[x],l,mid,val);
}else{insert(rs[x],mid+1,r,val);}
t[x]=t[ls[x]]+t[rs[x]];
}
Insert
用于更改变量. 先复制一份节点, 然后递归更改.
需要注意的是, 为了保存不同版本线段树根的编号, 在初次更新/建树时需要按照以下方式传参:
rt[x]=rt[x-1];
//不一定是从x-1上复制节点,依据题干,在哪个版本上修改,就复制哪个根的信息
insert(rt[x],0,INF,a);
首先, 先复制一份
然后, 直接向内传 rt[x]
(
r
o
o
t
x
root_x
rootx ,
x
x
x 版本线段树的根). 通过 &x
的特性, 可以直接更改 rt[x]
的编号
注 : rt[x]
与t[x]
不同, rt只存储节点编号,
x
x
x 代表线段树版本. 而t储存的是第
x
x
x 个节点的权值.
Scode
#include <bits/stdc++.h>
using namespace std;
namespace Radon{
void Main();
}
int main(){
Radon::Main();
return 0;
}
namespace Radon{
#define INF 1000000010
#define N 200010
#define vvv 5
int n,m;
int rt[N<<vvv];
int t[N<<vvv];
int ls[N<<vvv],rs[N<<vvv];
int cnt=0;
int clone(int x){
t[++cnt]=t[x];
ls[cnt]=ls[x];
rs[cnt]=rs[x];
return cnt;
}
void build(int &x,int l,int r,int num,int val){
if(x==0){
x=++cnt;
}
if(l==r){
t[x]=val;
return;
}
int mid=(l+r)>>1;
if(num<=mid){
build(ls[x],l,mid,num,val);
}
if(mid<r){
build(rs[x],mid+1,r,num,val);
}
t[x]=t[ls[x]]+t[rs[x]];
}
void insert(int &x,int l,int r,int val){
x=clone(x);
if(l==r){
t[x]++;
return;
}
int mid=(l+r)>>1;
if(val<=mid){
insert(ls[x],l,mid,val);
}else{
insert(rs[x],mid+1,r,val);
}
t[x]=t[ls[x]]+t[rs[x]];
}
int query(int xr,int xl,int l,int r,int k){
// cout << xr << ' ' << xl << ':' << l << ' ' << r << ' ' << k << endl;
if(l>=r){
return l;
}
int mid=(l+r)>>1;
int num=t[ls[xr]]-t[ls[xl]];
if(num>=k){
return query(ls[xr],ls[xl],l,mid,k);
}else{
return query(rs[xr],rs[xl],mid+1,r,k-num);
}
}
void Main(){
// freopen("")
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin >> n >> m;
for(int x=1;x<=n;x++){
int a;
cin >> a;
// rt[x]=rt[x-1];
// rt[x]=cnt+1;
rt[x]=rt[x-1];
insert(rt[x],0,INF,a);
}
// cout << "___________________________\n";
for(int x=1;x<=m;x++){
int a,b,c;
cin >> a >> b >> c;
cout << query(rt[b],rt[a-1],0,INF,c) << '\n';
}
}
}
标记永久化正在学习, 马上补