线段树
简介
(真的是简介,主要是我懒得写)
线段树:用来求一些区间问题,一种比较好理解代码也不难写的数据结构。
线段树,顾名思义,就是一棵由线段组成的树。每个线段就是一个区间。最下面的叶子结点的区间长度是(1),往上两个区间一合并,最后合并成一个区间。
每个区间的左儿子编号是该区间的编号乘(2),右儿子编号是左儿子编号加一(有的话)
线段树模板
线段树支持区间加,区间减(和加一样),区间求和blablabla
单点加、减和区间一样处理就好了
这里我们设置一个(tag)数组。因为我们比较懒,有的时候会发现,它让我们修改的这一整段区间已经被我们看到了,这段区间下面的我们不管了,直接在这段区间上记录我们要修改的值,用到的时候在说,用不到就不管他了。
每次修改/查询的时候,就二分找区间,找到了就返回,不在范围内就返回,要不然递归找左右区间。
细节看代码吧。
//区间加,区间求和
#include<iostream>
#include<cstdio>
using namespace std;
long long read(){
long long x=0;int f=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
return f?-x:x;
}
long long n,m,tag[400005],sum[400005];//区间和
#define lc u<<1//左儿子
#define rc u<<1|1//右儿子
#define mid (l+r)>>1//中点
void build(int u,int l,int r){//当前节点,左边界,右边界
if(l==r){sum[u]=read();return;}//如果这是一个叶子结点,直接读入
build(lc,l,mid),build(rc,(mid)+1,r);//递归左右儿子
sum[u]=sum[lc]+sum[rc];//pushup,更新一下当前节点
}
void pushdown(int u,int len){//下放标记
sum[lc]+=tag[u]*(len-(len>>1)),sum[rc]+=tag[u]*(len>>1);
tag[lc]+=tag[u],tag[rc]+=tag[u],tag[u]=0;
}
void add(int u,int l,int r,int L,int R,int x){
if(l>R||r<L) return;//如果当前区间不在询问范围内,返回
if(l>=L&&r<=R){tag[u]+=x,sum[u]+=(r-l+1)*x;return;}//若当前区间被询问覆盖,标记,返回
if(tag[u]) pushdown(u,r-l+1);//下放标记
add(lc,l,mid,L,R,x),add(rc,(mid)+1,r,L,R,x);//递归左右区间
sum[u]=sum[lc]+sum[rc];//更新
}
long long find(int u,int l,int r,int L,int R){
if(l>R||r<L) return 0;
if(l>=L&&r<=R) return sum[u];
if(tag[u]) pushdown(u,r-l+1);
return find(lc,l,mid,L,R)+find(rc,(mid)+1,r,L,R);
}
int main(){
n=read(),m=read();
build(1,1,n);//建立线段树
while(m--){
int k=read(),x,y;
if(k==1) x=read(),y=read(),k=read(),add(1,1,n,x,y,k);
else x=read(),y=read(),printf("%lld
",find(1,1,n,x,y));
}
return 0;
}
权值线段树动态开点合并
看着这名字很毒瘤,咳,分解一下。权值线段树,线段树动态开点,权值线段树合并。
其实不难,看一看嘛。
权值线段树
所谓权值线段树,就是记录一个数出现过多少次,而不是像以前那样记录具体数值。所以权值线段树需要开的个数是所有可能值中最大的值。比如题目有可能给你(1-100000)之间的数,那你的线段树就要开(100000<<2)个点。
每次输入一个数,就相当于单点修改,给这个数的位置的值加一。
这玩意能干什么?求区间第(k)大。
比如,有两种操作。第一种是给当前序列加入一个数,第二种是求当前序列中第(k)大。当然可以(sort),但也可以用权值线段树做。下面的题就不能(sort)。
怎么找?询问区间,显然,在权值线段树中,左节点代表的数永远小于右节点。若当前点的值大于等于(k),则当前节点,则说明第(k)大的点一定在左节点中,递归左节点,去找左节点中第(k)大的,反之递归右节点,找右节点中第(k-size[lc])大的。
int query(int q,int l,int r,int k){
if(l==r) return l;
int mid=(l+r)>>1;
if(t[lc].num>=k) return query(lc,l,mid,k);
return query(rc,mid+1,r,k-t[lc].num);
}
动态开点
考虑一颗权值线段树长什么样?它记录的是每个数出现的次数。如果有10000000个可能的数,但实际上题目只会给出1000个数,那么大量的点会是0,我们空间过度浪费。
考虑能不能给每个数搞一棵线段树。显然可以。对每个数来说,与它有关的只是一条从根节点连下来的链,对于每个数我们只需要维护这条链就可以了,需要的空间是节点数*深度,也就是(Nlog(Max)),(Max)是最大的可能的数。
我们发现线段树除去那些乱七八糟的维护之后,最根本的是要知道它的两个儿子是谁。所以我们动态开点,维护一下左右儿子就好了。
//插入x
struct Dier{
int l,r,num;
}t[100005<<5];
#define lc t[q].l
#define rc t[q].r
void insert(int &q,int l,int r,int x){//节点编号需要传值
if(!q) q=++cnt;//如果没有,就给他个编号
if(l==r){t[q].num++;return;}//计数器加一
int mid=(l+r)>>1;
if(x<=mid) insert(lc,l,mid,x);//递归左右儿子
else insert(rc,mid+1,r,x);
pushup(q);
}
合并
考虑两个数。他们分别有一棵线段树,也就是两条链。这两条链从上往下必然有一些点是相同的,我们只需要将不同的点合并就好了。
每条链上的每个点,都只有左节点或只有右节点。将两条链平移,会发现他们有一部分是可重叠的,并且一定在最上面,然后从下面的某个点开始分叉。我们只需要将分叉的地方按照一定的大小顺序合并,最后就会合并到一条链上了。
int merge(int x,int y){
if(!x) return y;//若有一个点为空,就返回另一个点
if(!y) return x;
t[y].l=merge(t[x].l,t[y].l);//按照大小顺序合并
t[y].r=merge(t[x].r,t[y].r);
pushup(y);
return y;
}
以及代码
#include<iostream>
#include<cstdio>
using namespace std;
long long read(){
long long x=0;int f=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return f?-x:x;
}
int n,m,q;
int r[100005],id[100005],f[100005],root[100005],cnt;
//r:初始值 id:这个数是第几个数 f:并查集爸爸 root:当前值的线段树的根
int find(int x){
return f[x]==x?x:f[x]=find(f[x]);
}
struct Dier{
int l,r,num;
}t[100005<<5];//注意数组大小
#define lc t[q].l
#define rc t[q].r
void pushup(int q){
t[q].num=t[lc].num+t[rc].num;
}
void insert(int &q,int l,int r,int x){//插入
if(!q) q=++cnt;
if(l==r){t[q].num++;return;}
int mid=(l+r)>>1;
if(x<=mid) insert(lc,l,mid,x);
else insert(rc,mid+1,r,x);
pushup(q);
}
int merge(int x,int y){//合并
if(!x) return y;
if(!y) return x;
t[y].l=merge(t[x].l,t[y].l);
t[y].r=merge(t[x].r,t[y].r);
pushup(y);
return y;
}
int query(int q,int l,int r,int k){//查询
if(l==r) return l;
int mid=(l+r)>>1;
if(t[lc].num>=k) return query(lc,l,mid,k);
return query(rc,mid+1,r,k-t[lc].num);
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;++i){
r[i]=read();
id[r[i]]=i,f[i]=i,root[i]=++cnt;
insert(root[i],1,n,r[i]);//给这个数建一个线段树
}
for(int i=1,x,y;i<=m;++i){
x=read(),y=read();
x=find(x),y=find(y);
merge(root[x],root[y]),f[x]=y;//合并两点的根
}
q=read();
while(q--){
char c;cin>>c;
int x=read(),y=read();
if(c=='B'){//合并
x=find(x),y=find(y);
merge(root[x],root[y]),f[x]=y;
}
else{//询问
x=find(x);
if(t[root[x]].num<y) printf("-1
");
else printf("%d
",id[query(root[x],1,n,y)]);
}
}
return 0;
}