线段树——operator_

算法概述

线段树是一种支持各种区间操作、区间询问的数据结构,复杂度同树状数组,时空常数略大,但应用范围极广。

树状数组基于二进制,线段树则是基于分治。

我们怎么分治?讲究几个点:

1. 1. 1. 分成左右两部分递归

2. 2. 2. 考虑合并

3. 3. 3. 考虑边界情况

那线段树的写法已经呼之欲出了。

写法

下文均以求解区间和为例。

存树

线段树真的是一棵树(不像某个树状数组长得奇形怪状),而且大概率是接近完全二叉树的,所以我一般喜欢直接用完全二叉树的方法: p ∗ 2 p*2 p2 是左儿子, p ∗ 2 + 1 p*2+1 p2+1 是右儿子,下面均以这种形式存树。

int ls(int p) {return p<<1;}
int rs(int p) {return p<<1|1;}

线段树类似于可持久化分治(什么鬼?!),就是把分治的每一步存下来,那么一个分治函数需要什么?左边界和右边界,所以这就是每个线段树节点必存的东西,另外东西看题目需要啥,我们这里需要的是这个区间的和( $lazy $ t a g tag tag 见后文)。

struct QWQ {
    int l,r,d,laz;
};

合并(pushup)

分治中有一步叫合并区间,是递归完子树后要做的,其实就是更新区间和

void pushup(int p) {//合并
    t[p].d=t[ls(p)].d+t[rs(p)].d;
}

建树(build)

建树就是完全的分治,一步步来:

1. 1. 1. 分成左右两部分递归

2. 2. 2. 考虑合并:边界直接赋值,其它转上文 p u s h u p pushup pushup

3. 3. 3. 考虑边界情况:只有一个点,初始权值为数组本身

void build(int p,int l,int r) {//建树
    t[p].l=l,t[p].r=r;
    if(l==r) {t[p].d=a[l];return;}
    int m=(l+r)>>1;
    build(ls(p),l,m);build(rs(p),m+1,r);
    pushup(p);
}

询问(query)

仍然是简单的分治,但注意第一步并不是都要执行,一步步来:

1. 1. 1. 分成左右两部分递归:如果一个儿子区间完全在询问区间之外,那么显然没必要递归徒增复杂度

2. 2. 2. 考虑合并:返回递归的返回值之和

3. 3. 3. 考虑边界情况:现区间完全包含于询问区间,返回存储的区间和

p u s h d o w n pushdown pushdown 转下文 p u s h d o w n pushdown pushdown

int query(int p,int l,int r) {//区间询问
    if(l<=t[p].l&&t[p].r<=r) return t[p].d;
    pushdown(p);
    int s=0,m=(t[p].l+t[p].r)>>1;
    if(l<=m) s+=query(ls(p),l,r);
    if(r>m) s+=query(rs(p),l,r);
    return s;
}

修改(update)

单点

(如果是单点,请自动忽略上文与下文 p u s h d o w n pushdown pushdown l a z laz laz

单点就简单了:

1. 1. 1. 分成左右两部分递归:如果一个儿子区间不包含修改的点之外,那么显然没必要递归徒增复杂度

2. 2. 2. 考虑合并:转上文 p u s h u p pushup pushup

3. 3. 3. 考虑边界情况:现区间就是询问的点,直接修改

void update(int p,int w,int v) {//单点修改
    if(t[p].l==w&&t[p].r==w) {t[p].d+=v;return;}
    int m=(t[p].l+t[p].r)>>1;
    if(w<=m) update(ls(p),w,v);
    else update(rs(p),w,v);
    pushup(p);
}
区间

现在起引入 l a z y lazy lazy t a g tag tag

如果区间修改我们也按和单点一样的方法,那么相当于区间中的每个点都要修改一遍,还不如暴力。

所以有一个神奇的东西叫懒惰标记。

考虑一下,如果有一个毒瘤出题人出了一组数据,全是从 1 − n 1-n 1n + 1 +1 +1 ,那么你修改了也没用到过啊,因为根本就没有询问,那么你为什么要修改呢?所以懒惰标记的思想就是:你要用了,我再修改也不迟。

(所以现在就能理解区间询问的 p u s h d o w n pushdown pushdown 了吧)

区间修改时借鉴区间询问的方法,找到一个整体区间后就停止,修改这个区间的懒惰标记。

1. 1. 1. 分成左右两部分递归:如果一个儿子区间完全在询问区间之外,那么显然没必要递归徒增复杂度

2. 2. 2. 考虑合并:转上文 p u s h u p pushup pushup

3. 3. 3. 考虑边界情况:现区间完全包含于询问区间,更新区间和与懒惰标记

关于为什么要 p u s h u p pushup pushup :因为我们的区间和是实时更新的,所以父亲的区间和也要更新。

关于为什么要 p u s h d o w n pushdown pushdown :因为我们约定 l a z y lazy lazy t a g tag tag 是给儿子看的,所以为了避免 p u s h d o w n pushdown pushdown 混淆这里也要来一次,详转下文 p u s h d o w n pushdown pushdown

void update(int p,int l,int r,int v) {//区间修改
    if(l<=t[p].l&&t[p].r<=r) {
        t[p].d+=(t[p].r-t[p].l+1)*v,t[p].laz+=v;
        return;
    }
    pushdown(p);
    int m=(t[p].l+t[p].r)>>1
    if(l<=m) update(ls(p),l,r,v);
    if(r>m) update(rs(p),l,r,v);
    pushup(p);
}

下放(pushdown)

用处是把父亲的懒惰标记下放到儿子上,并相应地更新。

void pushdown(int p) {//下放
    t[ls(p)].d+=t[p].laz*(t[ls(p)].r-t[ls(p)].l+1);
    t[rs(p)].d+=t[p].laz*(t[rs(p)].r-t[rs(p)].l+1);
    t[ls(p)].laz+=t[p].laz,t[rs(p)].laz+=t[p].laz;
    t[p].laz=0;
}

注:代码虽然很好写,却是线段树的核心

代码

struct QWQ {int l,r,d,laz;};
struct ST {
  	QWQ t[4*100005];
	int ls(int p) {return p<<1;}
	int rs(int p) {return p<<1|1;}
	void pushup(int p) {//合并
        t[p].d=t[ls(p)].d+t[rs(p)].d;
    }
	void pushdown(int p) {//下放
		t[ls(p)].d+=t[p].laz*(t[ls(p)].r-t[ls(p)].l+1);
		t[rs(p)].d+=t[p].laz*(t[rs(p)].r-t[rs(p)].l+1);
		t[ls(p)].laz+=t[p].laz,t[rs(p)].laz+=t[p].laz;
		t[p].laz=0;
	}
	void build(int p,int l,int r) {//建树
		t[p].l=l,t[p].r=r;
		if(l==r) {t[p].d=a[l];return;}
		int m=(l+r)>>1;
		build(ls(p),l,m);build(rs(p),m+1,r);
		pushup(p);
	}
	void update(int p,int l,int r,int v) {//区间修改
		if(l<=t[p].l&&t[p].r<=r) {
			t[p].d+=(t[p].r-t[p].l+1)*v,t[p].laz+=v;
			return;
		}
		pushdown(p);
		int m=(t[p].l+t[p].r)>>1
		if(l<=m) update(ls(p),l,r,v);
		if(r>m) update(rs(p),l,r,v);
		pushup(p);
	}
	int query(int p,int l,int r) {//区间询问
		if(l<=t[p].l&&t[p].r<=r) return t[p].d;
		pushdown(p);
		int s=0,m=(t[p].l+t[p].r)>>1;
		if(l<=m) s+=query(ls(p),l,r);
		if(r>m) s+=query(rs(p),l,r);
		return s;
	}
};
//所有l,r均为询问的区间,p为1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值