Segment-Tree

Segment Tree

  线段树(Segment Tree)是个用空间换时间的数据结构,可以用于区间的染色、查询、求和等操作。线段树从名字就可以看出是对于一个一维线段的操作,将原本逐个遍历的操作进行优化。

  首先,线段树的构成其实是一种二叉树,树的根节点是一整个区间,而叶子则是区间的最小单位,每个父节点是两个子节点的合并区间,并且两个子节点区间是对半分的。

    (1,4)
   /     \
 (1,2)  (3,4)
 /  |    |  \
1   2    3   4

  以[1,4]这个区间为例大概是这种感觉。但这些只是节点的编号,并非权值,每个 父节点的权值就是子节点的权值相加,因此可以通过这一点进行建树和求和优化。

  因此利用上述线段树的本质构成,可以进行线段树的建立,一维的数组分布在树的叶子上,而父节点是子节点的和,那么我们必然要先创建叶子,此时就需要按照dfs序。对于一个非叶节点先创建他的两个子节点,然后在回溯的时候利用求和得到该节点。代码如下:

struct node{
    int l,r,lz,sum;
}node[N];

void create(int n,int l,int r){
    node[n].l=l;
    node[n].r=r;
    if(l==r){
        node[n].sum=a[cnt++];//a数组是所求的区间数组,按序赋值给叶节点
        return;
    }
    int mid=(l+r)>>1;
    create(n<<1,l,mid);//根据二叉树本身的规律就会发现以横向顺序来看,父节点是第n个,那么他的两个子节点分别是第2n和2n+1个
    create(n<<1+1,mid+1,r);
    node[n].sum+=(node[n<<1].sum+node[n<<1+1].sum);//回溯时构成中间的节点
}

  接下来是线段树的几种操作。首先是查询操作,其实也就是求和,查询一段内包含区间的总权值。

  求一段给定区间的和首先从区间最大的根节点开始,在遇到每一个节点时进行判断,这个节点的左边界和右边界是否全部包含在给定区间内。如果是,那么ans加上这个节点的权值,然后回溯,不需要继续搜索他的子树;如果不是,那么在此判断,该节点的两个子节点是否与给定区间有交集,如果是则进行搜索,并重复上述操作进行递归。

  此处搜索子节点的判断:取得父节点的区间中值,如果给定区间的左边界小于等于中值,则左子节点有交集,若给定区间的右边界大于中值,则右子节点有交集。代码如下:

int search(int n,int l,int r){
    int ans=0;
    if(node[n].l>=l&&node[n].r<=r){//判断该节点是否完全包含于所求区间
        return node[n].sum;
    }
    int mid=(node[n].l+node[n].r)>>1;//取得区间中值
    if(l<=mid)ans+=search(n<<1,l,mid);
    if(r>mid)ans+=search(n<<1+1,mid+1,r);
    return ans;
}

  接下来是修改操作,就是使得区间内的每一个单点权值加上某一个数。

  这种操作其实还是一样的道理,从根节点开始判断,如果遇到一个节点完全包含于给定区间,那么直接对这个节点进行加权。当然,加权是针对单个点的,那么就需要知道这个节点包含多少最小点,那么这个节点的加权值就应该是给定值与节点区间长度的乘积。如果遇到非全包含的节点,则需要做出同上面查询操作一样的左右节点是否搜索判断。代码如下:

void add(int n,int k,int l,int r){
    if(node[n].l>=l&&node[n].r<=r){
        node[n].sum+=k*(node[n].r-node[n].l+1);
        return;
    }
    int mid=(node[n].l+node[n].r)>>1;
    if(l<=mid)add(n<<1,k,l,mid);//或者if(node[n].r>=l)
    if(r>mid)add(n<<1+1,k,mid+1,r);//或者if(node[n].l<=r)
}

  以上是两种基本操作,但是问题出现了,如果对于一个区间先进行修改操作,然后对于另外一个相交区间进行查询操作会怎么样?例如,我先对[1,3]中的每一个点进行+2修改,然后查询[2,4]的总权值。在前者操作中,由以上算法可知,我们实际上只对叶节点3进行了+2修改以及对(1,2)节点进行了+4修改。而在后者操作中,我们查询时求和的权值来源于叶节点2和(3,4)节点,然而这两个点并没有得到修改,这就造成了错误。那么如何进行优化呢?

  首先我们需要一个pushDown函数,其实在开头定义的node结构体中一直有个变量没有使用过,那就是lz(lazetag)。这个变量的作用就是记录对于该节点的修改,从而在下次查询该节点的子节点而非该节点本身时将lz的值传递下去,修改子节点从而查询得到正确的值。pushDown的代码如下:

void pushDown(int n){
    if(!node[n].lz){
        //继续向下传递lz标记,因为查询的节点可能再更下面
        node[n<<1].lz=node[n].lz;
        node[n<<1+1].lz=node[n].lz;
        //下面需要将每个子节点的权值也相应加权,保证查询正确
        node[n<<1].sum+=node[n].lz*(node[n<<1].r-node[n<<1].l+1);
        node[n<<1+1].sum+=node[n].lz*(node[n<<1+1].r-node[n<<1+1].l+1);
    	node[n].lz=0;//父节点lz标记归零防止重复
    }
}

  这就完成了函数的准备,还需要解决一个问题(3,4)节点的查询没有得到来自节点3的修改。这个其实自要在修改时的递归回溯中重新利用建树时的子节点求和操作就行了。

代码如下:

int search(int n,int l,int r){
    int ans=0;
    if(node[n].l>=l&&node[n].r<=r){//判断该节点是否完全包含于所求区间
        return node[n].sum;
    }
    pushDown(n);//在向下查询前进行标记下推操作,防止出错
    int mid=(node[n].l+node[n].r)>>1;//取得区间中值
    if(l<=mid)ans+=search(n<<1,l,mid);
    if(r>mid)ans+=search(n<<1+1,mid+1,r);
    return ans;
}

void add(int n,int k,int l,int r){
    if(node[n].l>=l&&node[n].r<=r){
        node[n].sum+=k*(node[n].r-node[n].l+1);
        node[n].lz=k;//标记lz变量
        return;
    }
    pushDown(n);//修改操作中使用这个函数其实在第一次修改中看起来没有用,但如果进行多次修改就会用上
    int mid=(node[n].l+node[n].r)>>1;
    if(l<=mid)add(n<<1,k,l,mid);//或者if(node[n].r>=l)
    if(r>mid)add(n<<1+1,k,mid+1,r);//或者if(node[n].l<=r)
    node[n].sum=node[n<<1].sum+node[n<<1+1].sum;//回溯求和更新中间节点
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值