zoukankan      html  css  js  c++  java
  • 数据结构——线段树

    前置知识

    二叉树

    正篇

    首先先来一个问题:给出一个长度为n的序列和m个操作,操作分别是:

    1. 单点修改
    2. 单点查询
    3. 区间加减
    4. 区间查询和

    模板题出处

    最简单的做法就是在数组上暴力for, 这样的话单点修改和查询的时间复杂度是(O(1)), 区间加减和区间查询的复杂度是(O(n)), 这样写的话简单是简单,但是会出现这样的情况:

    p1

    (纯暴力居然还有70???我还以为只有30呢)

    如图所示,就会T飞。

    那么我们怎么才能加速这一过程呢,答案就是线段树

    线段树可以用来维护区间信息,例如区间和,RMQ等,更进一步还有权值线段树和可持久化线段树(还有重口味线段树)......是一种用处比较多的数据结构,但是要注意,有些区间问题线段树是无法解决的,比如说区间众数。

    emmm......鬼扯这么多,先来看一下线段树的基本结构吧。

    线段树首先是一颗二叉树,每一个节点维护的是一个区间的信息。比如说根节点维护的是[1,n],然后叶子节点维护的是[x,x]。并且线段树用的是二分的思想,也就是说对于一个维护信息([l,r])的节点,它的两个子节点分别维护的是 ([l,frac{l+r}{2}])([frac{l+r}{2}+1,r]),给一张图可以更加清晰地看出这种结构:

    p2

    从图中可以很清晰地看出,实际上就是用一些线段去覆盖一个区间,线段树也因此得名

    那么,我们该怎么写线段树呢?

    首先,我们需要做一些准备工作:

    #include <bits/stdc++.h>
    #define ll long long
    using namespace std;
    const int maxn=200010;
    struct node{
    	ll sum,tag;
    }t[maxn<<2];//线段树
    ll arr[maxn];//原数组
    

    这里的空间必须要开大一点,并且还用用long long

    结构体里面的tag暂时不用管,以后再说具体有什么用

    接着,我们写一个维护节点的函数,实际上就是把子节点的值传递到父节点里面去:

    void update(ll pos){
    	t[pos].sum=t[pos<<1].sum+t[pos<<1|1].sum;
    }
    

    然后就开始build(建树)函数了:

    void build(ll l,ll r,ll pos){
    	if(l==r){
    		t[pos].sum=arr[l];
    		return;
    	}
    	ll mid=(l+r)/2;
    	build(l,mid,pos<<1);
    	build(mid+1,r,pos<<1|1);
    	update(pos);
    }
    

    这里面的 l 和 r 分别对应的是二分的过程中需要的左右区间,pos,对应的是在树里面的编号。

    当l==r的时候,就是递归到叶子节点的时候,此时我们直接填数字

    否则取一个mid值然后接着递归

    因为我们知道,对于一棵二叉树来讲,非叶节点n的左儿子的编号是(2n),右节点的编号是(2n+1),所以我们可以很快地写出递归的调用

    要注意的是,在递归返回的时候要用上面写的update函数去维护区间和

    建树完成后相对应的就是查询了。查询分为单点查询和区间查询,但是我们可以发现,单点查询其实就是查询区间 [ x , x ] ,所以我们只用写一个区间查询的就好了

    区间查询本质上就是对在原数组上的区间 [ l , r ],在线段树上找 l 和 r 分别对应的点。这里我们使用二分法来寻找。

    就这上面的那个图来看,如果我们只是查找 [ 5 , 7 ]或者 [ 1 , 4 ]那还好,但是如果我们查找的是[ 4 , 6 ]这种区间的话该怎么办呢?

    其实我们可以在二分的过程中把[ 4 , 6 ]拆成[ 4 , 4 ]和[ 5 , 6 ],然后直接统计和就好了。

    感觉直接看代码会直接一点:

    ll query(ll tl,ll tr,ll l,ll r,ll pos){
    	if(tl<=l&&tr>=r){
    		return t[pos].sum;
    	}
    	if(tl>r||tr<l) return 0;
    	ll mid=(l+r)/2;
    	//pushdown(l,r,pos);
        //这里的pushdown就当不存在哈...以后会解释的
    	return query(tl,tr,l,mid,pos<<1)+query(tl,tr,mid+1,r,pos<<1|1);
    }
    

    tl和tr是查寻区间,l和r是我们要二分的区间,pos对应的是线段树内部的位置

    很明显,如果tl<=l&&tr>=r的时候,就代表着此时我们的二分到的区间是在查询区间内部的,那么我们就没必要继续二分下去了,可以直接返回。

    否则的话,就看看我们二分到的区间是不是完全不在查询区间内部,比如说这种情况:

    image-20200408094534346

    还是查询 [ 4 , 6 ],所以我们在二分的过程中会出现途中黄(橙?)色的区间,但是这些区间都不会对答案有所贡献,也就是说完全在查询区间外(和插查询区间没有交集),这个时候我们也没必要沿着它们继续二分了,直接返回0就好了

    最后把搞出来的区间答案合并一下返回就没了

    区间修改

    由于线段树每一个节点都是维护的区间信息,所以如果我们只是简单地用单点查询的思路去修改,时间复杂度就会飙到(O(nlogn)),很明显连数组都不如,那么为了加速这一过程,我们会使用一个技巧——懒惰标记(lazy_tag)

    懒惰和懒惰标记

    懒惰的本质就是:

    p3

    打个比方。我们区间查询的时候会发现,如果我们二分到某一个区间内,发现这个区间被查询区间完全覆盖了,此时我们是直接返回的,并没有继续二分下去,因为继续二分是没必要的。同样的,我们如果也是对这一个区间进行区间加的话,我们其实可以用(O(1))的时间内算出整个区间在区间加之后的区间和,就是sum=sum+(r-l+1)*v;,那么我们极端一点想,如果对于以后的所有查询,这个区间都是被完全覆盖在查询区间内的话(也就是说每次程序跑到这里都会直接返回),那么我们为什么要在区间加的时候在这个区间底下继续二分呢?这就是懒惰(惰性)操作的一个基本的概念。

    但是事事没法像我们假设的那样如此极端,同样,某一个区间总会在某一次询问的时候被残忍地分割开来。此时就出问题了,我们当初没有对这个区间底下的子节点进行真正意义上的加减,那么我们还怎么获得正确的答案呢?答案就是继续懒惰。简单来讲就是query查到哪里我们就懒到哪里,query如果继续在这个区间内二分的话我们就继续更新其对应的区间,query在哪里停下我们就在那里停下,绝对不会更新多余的节点。

    给一张图看一下吧:

    p5

    假如我们要区间加的是如图所示的绿色区间,我们只用给两边的 [ 2 , 2 ] 和 [ 5 , 5 ]分别+v,然后给 [ 3 , 4 ]加上(4-3+1)*v就好了。

    如果我们查询 [ 2 , 4 ]的时候我们也不需要管 [ 3 , 3 ] 和 [ 4 , 4 ] 是不是被更新到了,因为反正 [ 3 , 4 ] 是被更新了的,那就直接拿来和 [ 2 , 4 ]合并一下就好了

    如果以后我们要查询 [ 4 , 6 ]的话我们就先把 [ 3 , 4 ] 懒惰的结果给传下来用来更新 [ 3 , 3 ] 和 [ 4 , 4 ],然后接下来就是一个简单地查询了

    那我们该怎么更新 [ 3 , 4 ] 的子节点呢?这个时候就是要用到懒惰标记了。

    懒惰标记(就是结构体里面的tag)记录的是从程序开始到此时query的时候区间内部发生的变化,可以理解为delta。打个比方,一开始build的时候tag都是0,因为此时就是初始状态。当对区间 [ l , r ] 区间+v(这里假设 [ l , r ] 是线段树内一个节点对应的区间),[ l , r ] 的tag就会+(r-l+1)*v,同样适用于其他节点。

    那么怎么才能懒下去呢?我们可以这么想,如果区间([l,r])都+v了的话,那么区间 ([l,frac{l+r}{2}])([frac{l+r}{2}+1,r])肯定是一样的。我们直接把v给下推到这两个区间内就好了。

    放一下代码:

    void f(ll pos,ll l,ll r,ll k)
    {
        t[pos].tag=t[pos].tag+k;
        t[pos].sum=t[pos].sum+k*(r-l+1);
    }
    void pushdown(ll l,ll r,ll pos){
    	if(!t[pos].tag) return;
    	ll mid=(l+r)/2;
    	f(pos<<1,l,mid,t[pos].tag);
    	f(pos<<1|1,mid+1,r,t[pos].tag);
    	t[pos].tag=0;
    }
    

    个人觉得还是很好理解的。

    这里要注意,由于pushdown是一个自上而下的过程,所以我们要在query和add的时候优先pushdown,否则的话就会更新错一些信息

    模板题AC代码:

    #include <bits/stdc++.h>
    #define ll long long
    using namespace std;
    const int maxn=200010;
    struct node{
    	ll sum,tag;
    }t[maxn<<2];
    ll arr[maxn];
    void update(ll pos){
    	t[pos].sum=t[pos<<1].sum+t[pos<<1|1].sum;
    }
    void build(ll l,ll r,ll pos){
    	if(l==r){
    		t[pos].sum=arr[l];
    		return;
    	}
    	ll mid=(l+r)/2;
    	build(l,mid,pos<<1);
    	build(mid+1,r,pos<<1|1);
    	update(pos);
    }
    void f(ll pos,ll l,ll r,ll k)
    {
        t[pos].tag=t[pos].tag+k;
        t[pos].sum=t[pos].sum+k*(r-l+1);
    }
    void pushdown(ll l,ll r,ll pos){
    	if(!t[pos].tag) return;
    	ll mid=(l+r)/2;
    	f(pos<<1,l,mid,t[pos].tag);
    	f(pos<<1|1,mid+1,r,t[pos].tag);
    	t[pos].tag=0;
    }
    void add(ll tl,ll tr,ll l,ll r,ll v,ll pos){
    	if(tl<=l&&tr>=r){
    		t[pos].sum+=v*(r-l+1);
    		t[pos].tag+=v;
    		return;
    	}
    	if(r<tl||l>tr){
    		return;
    	}
    	ll mid=(l+r)/2;
    	pushdown(l,r,pos);
    	add(tl,tr,l,mid,v,pos<<1);
    	add(tl,tr,mid+1,r,v,pos<<1|1);
    	update(pos);
    }
    ll query(ll tl,ll tr,ll l,ll r,ll pos){
    	if(tl<=l&&tr>=r){
    		return t[pos].sum;
    	}
    	if(tl>r||tr<l) return 0;
    	ll mid=(l+r)/2;
    	pushdown(l,r,pos);
    	return query(tl,tr,l,mid,pos<<1)+query(tl,tr,mid+1,r,pos<<1|1);
    }
    int main(void){
    	ios::sync_with_stdio(false);
    	ll n,m;
    	cin>>n>>m;
    	for(int i=1;i<=n;i++){
    		cin>>arr[i];
    	}
    	build(1,n,1);
    	for(int i=1;i<=m;i++){
    		ll opt,x,y,k;
    		cin>>opt;
    		if(opt==1){
    			cin>>x>>y>>k;
    			add(x,y,1,n,k,1);
    		}else{
    			cin>>x>>y;
    			cout<<query(x,y,1,n,1)<<endl;
    		}
    	}
    }
    

    这就是线段树最基本的思想。线段树有很多种形式和用处,这里只讲一下最基本的用处

    emmm......推荐看一下这道题:线段树模板2

    就是要注意维护两个lazy_tag的时候传递的顺序和取模,剩下的和原来一样

    代码就不放了

  • 相关阅读:
    array_map()与array_shift()搭配使用 PK array_column()函数
    Educational Codeforces Round 8 D. Magic Numbers
    hdu 1171 Big Event in HDU
    hdu 2844 poj 1742 Coins
    hdu 3591 The trouble of Xiaoqian
    hdu 2079 选课时间
    hdu 2191 珍惜现在,感恩生活 多重背包入门题
    hdu 5429 Geometric Progression 高精度浮点数(java版本)
    【BZOJ】1002: [FJOI2007]轮状病毒 递推+高精度
    hdu::1002 A + B Problem II
  • 原文地址:https://www.cnblogs.com/jrdxy/p/12663810.html
Copyright © 2011-2022 走看看