线段树基础

问题描述

设想一个区间上的问题,单点修改值,动态查询某区间的和

0.朴素算法

直接暴力来,区间上一个数一个数枚举求和(标题的“朴素”表达得很委婉)
假如查询次数是m,区间平均长度是n,那么时间复杂度达到了惊人的O(m*n)
这是不能接受的,因为O(1)修改,O(n)单次查询
为了在大数据规模下能够均衡,我们可以采用分治思想,
诞生了线段树(正如其名,是用在线段):O(log n)修改,O(log n)查询

接下来,你将看到的是线段树的进阶之路

1.单点修改,区间查询

线段树最初的样子,沿路修改,沿路合并区间查询,完全没有技术含量,直接上代码了

区间划分,回溯建树
建树即维护好区间和

//建树
void build(int i,int l,int r)
{
	if(l>r) return;
	//length=i>length? i:length; //记录树的编号范围,便于输出树的信息
	if(l==r){
		sum[i]=num[l];
		return;
	}
	
	int mid=(l+r)/2;
	build(i*2,l,mid);
	build(i*2+1,mid+1,r);
	
	sum[i]=sum[i*2]+sum[i*2+1];
}

单点修改,搜哪里“进入”哪里

//单点修改
void change(int i,int l,int r,int x,int k)
{
	if(l>r) return;
	if(l==r)
	{
		sum[i]+=k; //叶子节点单点加
		return;
	}
	
	int mid=(l+r)/2;
	if(x<=mid) change(i*2,l,mid,x,k);
	else change(i*2+1,mid+1,r,x,k);
	
	sum[i]=sum[i*2]+sum[i*2+1]; //回溯时更新
}

区间查询,查询哪里“进入”哪里“收集”起来

//区间查询
int query_sum(int i,int l,int r,int x,int y)
{
	if(l>y||r<x) return 0;
	if(l>=x&&r<=y) return sum[i];
	
	int mid=(l+r)/2;
	return query_sum(i*2,l,mid,x,y)+query_sum(i*2+1,mid+1,r,x,y);
}

2.区间修改,单点查询

区间修改假如暴力地使用m次单点修改,那么时间复杂度是不能接受的,为了节约时间,我们可以不必真正地全部修改区间的每一个元素,而是给路径上的某个祖先节点打标记,那样走这条路的时候就知道某个元素所在的区间曾经发生过区间加,这个标记我称为addtag
对于区间的修改,仿照区间的查询,并在全部包含在给定区间的节点打标记,然后立刻return,这一步是为了防止重复加
但是这样是有缺点的,只能单点查询,如果区间查询可能会存在一些问题,后面会马上讲到

//区间修改
void array_change(int i,int l,int r,int x,int y,int k)
{
	if(l>y||r<x) return;
	if(l>=x&&r<=y) {
		addtag[i]+=k;
		return;
	}
	
	int mid=(l+r)/2;
	array_change(i*2,l,mid,x,y,k);
	array_change(i*2+1,mid+1,r,x,y,k);
}
//单点查询
int query_num(int i,int l,int r,int x)
{
	if(l>r) return 0;
	if(l==r) return addtag[i]+sum[i];
	
	int mid=(l+r)/2;
	if(x<=mid) return addtag[i]+query_num(i*2,l,mid,x);
	else return addtag[i]+query_num(i*2+1,mid+1,r,x);
}

3.区间修改,区间查询

上述代码在单点查询时是没有问题的,但是区间查询就会出现问题
如果我们和1中的区间查询一样,区间只找完全符合的区间纳入结果,那么可能会出现:一个节点打了addtag标记,但是我无法完美地使用这个节点(它表示的区间并不是完全在给定区间内),必须使用这个节点的子节点,而子节点是没有标记的,子节点是存在问题的,反过来,祖先节点也是存在问题的,当查询区间和时,如果祖先节点完美调用了,那么就不需要打了标记的子节点了,但是祖先节点没有打过这个标记,也会使结果错误

那么解决这个问题有两种方式(均是对标记的手段进行优化):标记下放标记永久化
两种方法都维护了祖先节点的正确性,标记下放是靠回溯类似建树,标记永久化是沿路直接计算影响
不同之处在于标记下放的标记会下放,标记永久化不会下放

标记下放

标记下放的核心思想在于:时刻维护已走节点的正确性(包括当前所在的节点),未走的节点尽可能地懒惰不算以节约时间

核心原理: 区间修改时,每次使该节点sum值更新,然后打一个lazytag接着回溯
也就是说,对于每个点,如果有lazytag标记,那么它的值是完美的(更新过的),可以直接调用
只有我无法完美地调用这个节点时,不得已把之前偷懒的标记(lazytag)下放,此时子节点的sum值被修改正确,lazytag传给了子节点(甚至子节点也竭尽全力地偷懒),那么要记得把这个点的lazytag清空

由于区间修改和区间查询都会走树的路径,而且为了区间修改的最终结果同一条路径上不会有多个lazytag,现在示例是区间加法,仅区间查询时下放效果相同,但是如果维护的是别的信息,很可能会致错,而且标记下放和区间修改、区间查询同步发生改变的是常数,不改变理论时间复杂度,所以我们选择区间修改、区间查询都下放标记

工具性的

//对节点加
void add(int i,int l,int r,int x)
{
	lazytag[i]+=x;
	sum[i]+=(r-l+1)*x;
}

//标记下放
void pushdown(int i,int l,int r)
{
	if(!lazytag) return;
	int mid=(l+r)/2;
	add(i*2,l,mid,lazytag[i]);
	add(i*2+1,mid+1,r,lazytag[i]);
	lazytag[i]=0; //清空
}

区间修改

//区间修改
void array_change(int i,int l,int r,int x,int y,int k)
{
	if(l>y||r<x) return;
	if(l>=x&&r<=y) {
		add(i,l,r,k);
		return;
	}
	pushdown(i,l,r);
	int mid=(l+r)/2;
	array_change(i*2,l,mid,x,y,k);
	array_change(i*2+1,mid+1,r,x,y,k);
	sum[i]=sum[i*2]+sum[i*2+1];
}

区间查询

//区间查询
int query_sum(int i,int l,int r,int x,int y)
{
	if(l>=x&&r<=y) return sum[i];
	if(l>y||r<x) return 0;
	pushdown(i,l,r);
	int mid=(l+r)/2;
	return query_sum(i*2,l,mid,x,y)+query_sum(i*2+1,mid+1,r,x,y);
}

标记永久化

区间修改时沿路直接计算对当前节点的影响,以维护修改节点的祖先节点的正确性
对于修改的节点,仍然打标记,但是标记就这样永久化,以后查询的时候计算影响即可
和“区间修改,单点查询”不同点在于,祖先节点的正确性得到了维护

区间修改

//区间修改
void array_change(int i,int l,int r,int x,int y,int k)
{
	if(l>y||r<x) return;
	if(l>=x&&r<=y) {
		addtag[i]+=k;
		return;
	}
	sum[i]+=(min(r,y)-max(l,x)+1)*k;
	int mid=(l+r)/2;
	array_change(i*2,l,mid,x,y,k);
	array_change(i*2+1,mid+1,r,x,y,k);
}

区间查询

//区间查询
long long query_sum(int i,int l,int r,int x,int y)
{
	if(l>=x&&r<=y) return sum[i]+(r-l+1)*addtag[i];
	if(l>y||r<x) return 0;
	int mid=(l+r)/2;
	return query_sum(i*2,l,mid,x,y)+query_sum(i*2+1,mid+1,r,x,y)+(min(r,y)-max(x,l)+1)*addtag[i];
}
  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值