线段树

线段树

简介

(真的是简介,主要是我懒得写)

线段树:用来求一些区间问题,一种比较好理解代码也不难写的数据结构。

线段树,顾名思义,就是一棵由线段组成的树。每个线段就是一个区间。最下面的叶子结点的区间长度是\(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\n",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\n");
			else printf("%d\n",id[query(root[x],1,n,y)]);
		}
	}
	return 0;
}
posted @ 2018-11-08 17:15  Kylin_Seven  阅读(214)  评论(0编辑  收藏  举报