• 线段树


    线段树

    简介

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

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

    线段树,顾名思义,就是一棵由线段组成的树。每个线段就是一个区间。最下面的叶子结点的区间长度是(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;
    }
    
  • 相关阅读:
    5.MFC基础(五)视图、运行时类信息、动态创建
    4.MFC基础(四)菜单、工具栏、状态栏
    OpenCV Python 4.0安装
    windows批量导出文件名到txt
    *&p理解
    VS调试快捷键配置更改
    数组类的创建(下)
    数组类的创建(上)
    operator用法:隐式类型转换
    C++单例模式
  • 原文地址:https://www.cnblogs.com/kylinbalck/p/9930321.html
Copyright © 2020-2023  润新知