线段树的基本操作(入门必备)

8 篇文章 0 订阅
6 篇文章 0 订阅

线段树

线段树算是一种较为常用的数据结构,它在落谷中的定位是树形结构:
比如说
在这里插入图片描述
有趣

线段树的使用

线段树既然是一个算法(←废话),那么它一定有自身的用处。

经研究发现,计算最值的最佳算法是RMQ,O(1)的查询和O(nlogn)的预处理几乎是这类问题的时间复杂度缩短到最低,但是一旦进行了单点修改之后,就要重新进行一次处理。

而计算区间和的最优解是前缀和算法,O(1)的查询和O(n)的预处理为此提供了绝妙的先决条件。但如上而言,一旦进行单点修改,依然要重新预处理。对此,我们不得不引用线段树来解决这类问题。

结构预览

来源于网络

形似如此,单点修改的影响降至O(logn),每次查询为O(logn),建树时间也只在n的数量级上。

线段树的初级操作

建树

线段树的构建时间复杂度为O(n),空间占用为2n,(我也不知道网上为什么那么多人用4n的数组),常规运用递归实现。代码如下:

//建树
void bulid(int k,int l,int r)
{
    if(l == r)
    {
        ans[k] = maxx[k] = minn[k] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    bulid(k << 1, l, mid);
    bulid((k << 1)|1, mid+1, r);

    ans[k] = ans[k << 1] + ans[(k << 1)|1];
    maxx[k] = max(maxx[k << 1], maxx[(k << 1)|1]);
    minn[k] = min(minn[k << 1], minn[(k << 1)|1]);//预处理区间和和区间最值
}

查询

经常性使用的查询只用两种:区间最值以及区间和,这里也只对这两种进行一个讲解,有其他需求在下方留言,尽量添加。代码如下:

//区间最值查询
int RMQ(int k,int l,int r,int x,int y) {	//此为最大值查询,最小值查询同理,稍加修改即可
	if (l >= x&&r <= y) return maxx[k];
	int mid = (l + r) >> 1,res=0;
	if (mid >= x) 
		res = max(res, RMQ(k << 1,l,mid,x,y));
	if (mid < y) 
		res = max(res, RMQ((k << 1)|1,mid+1,r,x,y));
	return res;
}
//区间和查询
int RSQ(int k,int l,int r,int x,int y) {
	if (l >= x&&r <= y) return ans[k];
	if (l < y || r > x) return 0;
	int mid = (l + r) >> 1;
	return RSQ(k << 1,l,mid,x,y)+RSQ((k << 1)|1,mid+1,r,x,y);
}

因为在线段树中,一个区间被拆分成两个区间,这个区间的信息就是这两个自区间的信息总和,对这两个子区间的信息进行O(1)的处理就能得到父区间的信息,所以递归是最有做法。

单点修改

单点修改是在线段树中比较不常见的操作,因为它可以用区间修改来代替,但我们在这里还是稍微提一下比较好。代码如下:

//单点修改
void PC(int k,int l,int r,int p,int v) {
	if (l == r&&l == p) {
		a[l] = v;ans[k] = v;maxx[k] = v;minn[k] = v;
		return;
	}int mid = (l + r) >> 1;
	if (mid >= p) PC(k << 1,l,mid,p,v);
	else PC((k << 1)|1,mid+1,p,v);
	ans[k] = ans[k << 1] + ans[(k << 1)|1];
    maxx[k] = max(maxx[k << 1], maxx[(k << 1)|1]);
    minn[k] = min(minn[k << 1], minn[(k << 1)|1]);//再处理区间和和区间最值
}

易知该函数的时间为O(logn)

新的问题

区间更新

区间更新的是线段树的核心。更新区间即更新最底层的叶子结点,而上层的结点与叶子结点的值息息相关,牵一发而动全身。如果我们还是按建立线段树的方法,先向下递归,递归到叶子节点时更新值,再回溯上来整合信息,这种更新会很慢,而且某些结点的更新,可能对于我们暂时还用不上。那么这就浪费了极大的时间。

怎么处理这个问题呢?

问题处理

对于这个问题,常见的做法是标记化修改。给每个节点都引入一个tag延时标记来储存已经存在却暂时无用的修改。当我们遍历到一个延迟标记不为0的结点时,若要递归遍历其子节点,那么就在递归前进行一次延迟标记向下传递操作,更新其子节点的值和延迟标记,并清空父节点的延迟标记。这样,我们就可以加快区间更新的速度。很明显,它可以节省较多的时间。

代码实现

粗糙解法

//加法标记下传
void push(int k,int l,int r) {
	int mid = (l + r) >> 1;
	plus[k << 1] += plus[k];
	plus[(k << 1)|1] += plus[k]//延迟标记下传
	ans[k << 1] += plus[k] * (mid-l+1);
	ans[(k << 1)|1] += plus[k] * (r-mid);	//更新子区间和
	plus[k] = 0;		//清零标记
}

//乘法标记下传
void down(int k,int l,int r) {
	int mid = (l + r) >> 1;
	ride[k << 1] += ride[k];	//假装ride是乘法的乘
	ride[(k << 1)|1] += ride[k];
	ans[k << 1] *= ride[k];
	ans[(k << 1)|1] *= ride[k];
	ride[k]=0;
}
//区间更新
//加法
void add(int k,int l,int r,int x,int y,int v) {
	if (l >= x&&r <= y) {
		ans[k] += v * (y-x+1);
		plus[k] += v;
		return;
	}
	push(k,l,r);
	int mid = (l + r) >> 1;
	if (l <= mid)
		add(k << 1,l,mid,x,y,v);
	if(r > mid)
		add((k << 1)|1,mid+1,r,x,y,v);
	ans[k]=ans[k << 1]+ans[(k << 1)|1];
}
//乘法
void mult(int k,int l,int r,int x,int y,int v) {
	if (l >= x&&r <= y) {
		ans[k] *= v;
		ride[k] += v;
		return;
	}
	down(k,l,r);
	int mid = (l + r) >> 1;
	if (l <= mid)
		mult(k << 1,l,mid,x,y,v);
	if(r > mid)
		mult((k << 1)|1,mid+1,r,x,y,v);
	ans[k]=ans[k << 1]+ans[(k << 1)|1];
}

优质解法

void Add(int k,int l,int r,int v)		//
{
	add[k] += v;
	sum[k] += (r-l+1)*v;
	return;
}

void pushdown(int k,int l,int r,int mid)
{
	if(add[k]==0) return;
	Add(k<<1,l,mid,add[k]);
	Add((k<<1)|1,mid+1,r,add[k]);
	add[k]=0;
}

void modify(int k,int l,int r,int x,int y,int v) {
	if(l>=x&&r<=y) return Add(k,l,r,v);
	int mid=(l+r)>>1;
	pushdown(k,l,r,mid);
	if(x<=mid) modify(k<<1,l,mid,x,y,v);
	if(mid<y) modify((k<<1)|1,mid+1,r,x,y,v);
	sum[k]=sum[k<<1]+sum[(k<<1)|1];
}

上述只是加法的做法,当然,乘法只要在这个基础上稍作修改并且添加一句乘(加)法标记下传就可以,在此处也不在过多进行介绍,区间查询之前也有提到过,不多作解释。

ps:另外,解法的优劣全凭个人喜好而定,此处的分类只是个人看法,仅供参考。

标记永久化

对于区间修改,另外一种方法就是不下传标记,改为在询问过程中计算各节点对当前询问的影响。(然而对于这种方法,博主至今不知道如何处理好加与乘的关系)为了保证询问的复杂度,子节点的影响需要在修改操作时就计算好。因此实际上,sum的值表示这个区间内所有数共同加上的值,除了add之外。需要注意的是区间的add内可能有一部分在祖先上,这需要在递归时累加。
这种标记永久化的写法在树套树以及可持久化数据结构中较为方便。

code:

void modify(int k,int l,int r,int x,int y,int v){
	if(l >= x&&r <= y) {
		add[k] += v;
		return;
	}
	sum[k] +=(min(r,y)-max(l,x)+1)*v;
	int mid=(l+r)>>1;
	if(x <= mid) modify(k<<1,l,mid,x,y,v);
	if(mid<y) modify((k<<1)|1,mid+1,r,x,y,v);
}

int query(int k,int l,int r,int x,int y) {
	if(l >= x&&r <= y) return sum[k]+(r-l+1)*add[k];
	int mid=(l+r)>>1;
	int res=(min(r,y)-max(l,x)+1)*add[k];
	if(x <= mid) res += query(k<<1,l,mid,x,y);
	if(mid<y) res += query((k<<1)|1,mid+1,r,x,y);
	return res;
}

非递归实现

非递归的思路何其精妙,此处稍作提及,各位有兴趣可以去寻找清华大学张琨玮《统计的力量》。
非递归实现有什么优点呢?它的代码简单,特别是点修改和区间查询,速度快,建树简单,遍历简单。总之尽量使用非递归吧。
但如果题目要求支持区间修改,那么代码将会变得比较复杂,所以需要作一个取舍;不过如果是在所有修改结束后一次性推下所有标记,那么还是有可行性的。

那么不多说,先甩一波代码。

code:

ps:这里只囊括了几个简单操作。

建树
void build(int n){	//n为原数组长度,目的计算扩增序列长度
	N=1;while(N < n+2) N <<= 1;
	for (int i=1; i <= n; i++) sum[N+i]=a[i];
	for (int i=N-1; i > 0; --i) {
		sum[i]=sum[i<<1]+sum[(i<<1)|1];
		add[i]=0;
	}
}
单点修改
void updata(int p,int v) {
	for (int i=N+p; i; i >>= 1) 
		sum[i] += v;
}
区间修改
void update(int l,int r,int v){
	int s,t,ln=0,rn=0,x=1;
	for(s=N+l-1,t=N+r+1; s^t^1; s >>= 1,t >>= 1,x <<= 1){
		sum[s] += v*ln;
		sum[t] += C*rn;
		if(~s&1) add[s^1] += v,sum[s^1] += v*x,ln+=x;
		if( t&1) add[t^1] += v,sum[t^1] += v*x,rn+=x;
	}
	for(; s; s >>= 1,t >>= 1){
		sum[s] += v*ln;
		sum[t] += v*rn;
	} 
}
区间查询
int query(int l,int r){
	int s,t,ln=0,rn=0,x=1;
	int ans=0;
	for(s=N+l-1,t=N+r+1; s^t^1; s >>= 1,t >>= 1,x <<= 1){
		if(add[s]) ans += add[s]*ln;
		if(add[t]) ans += add[t]*rn;
		if(~s&1) ans += sum[s^1],ln += x;
		if( t&1) ans += sum[t^1],rn += x; 
	}
	for(; s; s >>= 1,t >>= 1){
		ans += add[s]*ln;
		ans += add[t]*rn;
	}
	return ans;
}
单点修改下的区间查询
int query(int L,int R){
	int ans=0;
	for(int s=N+l-1,t=N+r+1; s^t^1; s >>= 1,t >>= 1){
		if(~s&1) ans += sum[s^1];
		if( t&1) ans += sum[t^1];
	}
	return ans;
} 

结尾

那么,关于线段树的个人理解也就只有这么点了,以后应该会对扫描线和主席树作一个讲解,谢谢观看。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值