【总结】线段树

简介

"线段树"听起来很高大上对不对?但它的本质是这样的:

你会线段,你会树,所以你会线段树。

并没有开玩笑,线段树这玩意真的不难。

按照上面那句话来看,首先我们需要线段。假设这根线段为 \([1,10]\)

然后我们需要树。等等!线段跟树有什么关系?

见过切西瓜吧,我们不停地把西瓜从中间切开,切得更薄,直到切不动为止(起码我是这样切的,不知道为什么每次都切得很奇怪)。

线段树也是这样的,我们把这根线段不停地从中间分开,直到分不动为止。

对于一根线段,我们用 \(l\)\(r\) 来描述它。就像二分一样,我们令 \(mid=(l+r)\div2\) ,那么这根线段切成的两根较短的线段就是 \([l,mid]\)\([mid+1,r]\)

以上面那根 \([1,10]\) 的线段为例,它的树就长这样:

1,10

观察上图,这棵树的最下面都是 \(l==r\) 的单个点诶!

我们发现,线段树的叶子节点个数为线段的长度。

继续观察,啊,这棵树怎么满满当当的?

我们发现,线段树除去最后一层后,是一颗满二叉树。


种一棵 线段树 的步骤:

  • 有勉强还能说得过去的 二分 经验
  • 准备好 l、r、mid 等工具
  • 准备好 大喊"为什么我要学树状数组" 的精力

然后我们就可以开始种线段树了!

种树 建树

作为一颗优秀的二叉树,需要具备什么?

当然是左子树,右子树的编号和值!把它们存在数组里!

那么,这个存放着二叉树结点的数组需要开多大呢?也就是说,它们的编号怎么编,最大会是多少呢?

上面我们已经有了一个发现:

线段树除去最后一层后,是一颗满二叉树。

众所周知,满二叉树属于完全二叉树,所以,我们就可以按照完全二叉树的方式编号:

l=root<<1,r=l|1
// 此处l=root*2,为偶数
// 偶数|1=这个数+1

那么,最大可以编到几号呢?

一棵 \(n\) 个叶子结点的满二叉树最多有 \(n+\dfrac{n}{2}+\dfrac{n}{4}+...+2+1=2\times n-1\) 个结点,但这只是前面满二叉树的部分,再加上最后一层,编号最大可以达到大约 \(4\times n\)

解决了单个点,我们接着看怎么种树~

如果我们用线段树储存一个数组,那么此时线段树的左右端点就是某一段这个数组中的区间。树中的每个节点也需要有对应的值,这里我们以当前区间的最大值为例。

对于线段树中所有的叶子节点,也就是单个点,我们可以很简单的直接赋值成它的端点(左右都一样)在数组中对应的值——只有一个点,最大值肯定是自己呀!

如果不是单个点呢?我们就像切西瓜一样把它从中间切开,直到它是单个点为止咯~

如果一个区间可以继续切下去,我们就先继续切,直到只剩单个点,可以直接确定值为止。

由于这里是区间最大值,所以我们在建完左右子树后,将当前节点的值更新为左、右子树中的最大值。

Code

typedef long long ll;
const int maxn=1e5+5;
ll val[maxn];
struct segment_tree{
	ll l,r;
	ll max_data;
}a[maxn<<2];
ll max(ll x,ll y){
	return x>y?x:y;
}
void build(ll p,ll l,ll r){
	a[p].l=l,a[p].r=r;
	if(l==r){
		a[p].max_data=val[l];
		return;
	}
	ll mid=l+r>>1;
	build(p<<1,l,mid);
	build((p<<1)|1,mid+1,r);
	a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
	return;
}

那么此时 \([1,n]\) 的最大值就是 \(a[1].max\_data\) .

咦?那么像 \([1,9]\)\([2,8]\) 这样在树中不是完整节点的最大值呢?树状数组不是可以求任意区间嘛?

先等等,我们先把 单点修改区间查询(树状数组经典题目) 中的单点修改讲完。

单点修改,就像树状数组中的那样,修改一个点的话,就只用修改这个点和它所有的祖先的值就行了。

同样递归求解。

如果当前区间刚好就是将要更新的点(设这个点为 \(x\)),也就是说 \(l=r=x\) ,直接更新当前结点所对应的最大值。

如果 \(x\) 在当前 \([l,r]\) 的范围内,并且 \(l\ne r\) ,类似于二分地寻找 \(x\) 所对应的子树:

  • \(mid=l+r>>1\)
  • 如果 \(x<=mid\)
    • 递归更新 \([l,mid]\)
  • 否则
    • 递归更新 \([mid+1,r]\)
  • 更新当前结点( \(p\) )为 \(\max(a[p<<1].max\_data,a[(p<<1)+1].max\_data)\)

Code

typedef long long ll;
const int maxn=1e5+5;
struct segment_tree{
	ll l,r;
	ll max_data;
}a[maxn<<2];
ll max(ll x,ll y){
	return x>y?x:y;
}
void Update(ll p,ll x,ll v){
	if(a[p].l==a[p].r){
		a[p].max_data=v;
		return;
	}
	ll mid=a[p].l+a[p].r>>1;
	if(x<=mid)Update(p<<1,x,v);
	else Update((p<<1)|1,x,v);
	a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
	return;
}

最后,就可以开始区间查询了~

我们想,假设我们要查找区间 \([3,9]\) 的最大值,我们需要哪些结点的 \(max\_data\) ?

线段树

       :有部分在询问区间内,继续递归询问

       :完全不在询问区间内,不会被递归到

       :整个在询问区间内,会被递归到,不再继续向下递归

       :虽然在询问区间内,但自己的祖先也在询问区间内,不会被递归到。

我们把所有蓝色的区间放在一起,刚好就是我们的询问区间 \([3,9]\)

所以,大概过程就是这样的:

  • 如果当前区间在询问区间内
    • 返回当前结点的 \(max\_data\)
  • 否则
    • 如果当前节点的左子树包含一部分询问区间
      • 递归询问左子树
    • 如果当前结点的右子树包含一部分询问区间
      • 递归询问右子树
    • 返回两个询问中的最大值

Code

typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
struct segment_tree{
	ll l,r;
	ll max_data;
}a[maxn<<2];
ll max(ll x,ll y){
	return x>y?x:y;
}
ll Query(ll p,ll l,ll r){
	if(a[p].l>=l&&a[p].r<=r)
		return a[p].max_data;
	ll val=-inf;
	ll mid=a[p].l+a[p].r>>1;
	if(l<=mid)
		val=max(val,Query(p<<1,l,r));
	if(r>mid)
		val=max(val,Query((p<<1)|1,l,r));
	return val;
} 

完整代码

#include<cstdio>
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
ll val[maxn];
ll n,q,type,l,r,x,v;
struct segment_tree{
	ll l,r;
	ll max_data;
}a[maxn<<2];
void read(ll&x){
	x=0;
	bool f=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch&15);
		ch=getchar();
	}
	if(f)x=-x;
	return;
}
ll max(ll x,ll y){
	return x>y?x:y;
}
void build(ll p,ll l,ll r){
	a[p].l=l,a[p].r=r;
	if(l==r){
		a[p].max_data=val[l];
		return;
	}
	ll mid=l+r>>1;
	build(p<<1,l,mid);
	build((p<<1)|1,mid+1,r);
	a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
	return;
}
void Update(ll p,ll x,ll v){
	if(a[p].l==a[p].r){
		a[p].max_data=v;
		return;
	}
	ll mid=a[p].l+a[p].r>>1;
	if(x<=mid)Update(p<<1,x,v);
	else Update((p<<1)|1,x,v);
	a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
	return;
}
ll Query(ll p,ll l,ll r){
	if(a[p].l>=l&&a[p].r<=r)
		return a[p].max_data;
	ll val=-inf;
	ll mid=a[p].l+a[p].r>>1;
	if(l<=mid)
		val=max(val,Query(p<<1,l,r));
	if(r>mid)
		val=max(val,Query((p<<1)|1,l,r));
	return val;
} 
int main(){
	read(n);
	for(int i=1;i<=n;++i)
		read(val[i]);
	build(1,1,n);
	read(q);
	while(q--){
		read(type);
		if(type==1){
			read(x);read(v);
			Update(1,x,v);
		}
		else{
			read(l);read(r);
			printf("%lld\n",Query(1,l,r));
		}
	}
	return 0;
}

例题

单点修改,区间查询 (没有找到求区间最大值,又不想塞 RMQ 的题的我)

这里要求我们求区间和,而不是最大值,但很好改,我们只需要把 max 改成 + ,把 Update 中更新值的赋值改成 += ,把询问时求最大值返回改成求和返回就可以了。

#include<cstdio>
typedef long long ll;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
ll val[maxn];
ll n,q,type,l,r,x,v;
struct segment_tree{
	ll l,r;
	ll sum;
}a[maxn<<2];
void read(ll&x){
	x=0;
	bool f=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch&15);
		ch=getchar();
	}
	if(f)x=-x;
	return;
}
ll max(ll x,ll y){
	return x>y?x:y;
}
void build(ll p,ll l,ll r){
	a[p].l=l,a[p].r=r;
	if(l==r){
		a[p].sum=val[l];
		return;
	}
	ll mid=l+r>>1;
	build(p<<1,l,mid);
	build((p<<1)|1,mid+1,r);
	a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
	return;
}
void Update(ll p,ll x,ll v){
	if(a[p].l==a[p].r){
		a[p].sum+=v;
		return;
	}
	ll mid=a[p].l+a[p].r>>1;
	if(x<=mid)Update(p<<1,x,v);
	else Update((p<<1)|1,x,v);
	a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
	return;
}
ll Query(ll p,ll l,ll r){
	if(a[p].l>=l&&a[p].r<=r)
		return a[p].sum;
	ll val=0;
	ll mid=a[p].l+a[p].r>>1;
	if(l<=mid)
		val+=Query(p<<1,l,r);
	if(r>mid)
		val+=Query((p<<1)|1,l,r);
	return val;
} 
int main(){
	read(n);read(q);
	for(int i=1;i<=n;++i)
		read(val[i]);
	build(1,1,n);
	while(q--){
		read(type);
		if(type==1){
			read(x);read(v);
			Update(1,x,v);
		}
		else{
			read(l);read(r);
			printf("%lld\n",Query(1,l,r));
		}
	}
	return 0;
}

更上一层楼

例题

双倍经验

我们会感到疑惑:树状数组可以区间修改(虽然我直到现在都不会打),那线段树可不可以呢?

请不要小看线段树,它除了没有树状数组那么快,码量比树状数组大之外,哪儿都比树状数组好滴~

以前的区间修改,我们使用两个树状数组,一个维护差分数组,一个维护前缀和实现。

线段树自然也可以用差分,但是我们还有更魔幻的方法~

我们在每个结点中多存放一个懒惰标记(以下简称 “懒标”)。

当我们更新某个区间时(假设只更新一次),如果当前递归到的区间属于更新区间,我们就不继续更改每个点的值了,而是只让当前大区间的 \(sum\) 增加 区间长度*增加的值 。

这样,当前区间的值还是相当于整个区间的值。

但是,如果需要用到当前区间的子孙的值呢?它们并没有被更新呀?

这个时候就要用到懒标了,我们在更新时让懒标加上增加的值,也就是说,懒标存放的是这个区间帮自己的子树存的值。

在用到这个区间的子树时,我们将懒标向下拓展一层到这个区间的左子树与右子树,让左子树的 \(sum\) 存放 左子树的区间长度*懒标 ,右子树的 \(sum\) 存放 右子树的区间长度*懒标

同样,为了标记左子树与右子树帮它们的子树存放了值,我们将懒标赋给它们的懒标。

此时当前区间已经没有帮自己的子树存放值了,我们将它的懒标清空。

我们将上面的过程写在一个拓展( \(\operatorname{Spread}\) )函数中,当我们在更新或询问时,每次要向下递归时,为了保证当前节点没有帮自己的子树存放值,调用 \(\operatorname{Spread}\)

Code

#include<cstdio>
typedef long long ll;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
ll val[maxn];
ll n,q,type,l,r,x;
struct _{
	ll l,r;
	ll sum,add;
}a[maxn<<2];
void read(ll&x){
	x=0;
	bool f=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch&15);
		ch=getchar();
	}
	if(f)x=-x;
	return;
}
ll max(ll x,ll y){
	return x>y?x:y;
}
void Spread(ll p){
	if(a[p].add){
		ll lt=p<<1;
		ll rt=lt|1;
		a[lt].sum+=a[p].add*(a[lt].r-a[lt].l+1);
		a[rt].sum+=a[p].add*(a[rt].r-a[rt].l+1);
		a[lt].add+=a[p].add;
		a[rt].add+=a[p].add;
		a[p].add=0;
	}
	return;
}
void build(ll p,ll l,ll r){
	a[p].l=l,a[p].r=r;
	if(l==r){
		a[p].sum=val[l];
		return;
	}
	ll mid=l+r>>1;
	build(p<<1,l,mid);
	build((p<<1)|1,mid+1,r);
	a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
	return;
}
void Update(ll p,ll x,ll y,ll v){
	if(x<=a[p].l&&y>=a[p].r){
		a[p].sum+=v*(a[p].r-a[p].l+1);
		a[p].add+=v;
		return;
	}
	Spread(p);
	ll mid=a[p].l+a[p].r>>1;
	if(x<=mid)Update(p<<1,x,y,v);
	if(y>mid)Update((p<<1)|1,x,y,v);
	a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
	return;
}
ll Query(ll p,ll l,ll r){
	if(l<=a[p].l&&r>=a[p].r)
		return a[p].sum;
	ll val=0;
	Spread(p);
	ll mid=a[p].l+a[p].r>>1;
	if(l<=mid)
		val+=Query(p<<1,l,r);
	if(r>mid)
		val+=Query((p<<1)|1,l,r);
	return val;
} 
int main(){
	read(n);read(q);
	for(int i=1;i<=n;++i)
		read(val[i]);
	build(1,1,n);
	while(q--){
		read(type);read(l);read(r);
		if(type==1){
			read(x);
			Update(1,l,r,x);
		}
		else printf("%lld\n",Query(1,l,r));
	}
	return 0;
}

我**我起码也码了92行你告诉我这是道 普及/提高- ??这难道不是道 省选/NOI- 吗?

拓展

GSS 1 GSS 3

我们继续。

我们在一个结点中存放下列值:

  • 当前区间值的和( \(sum\) )
  • 当前区间包含左端点的最大连续子段和( \(lmax\) )
  • 当前区间包含右端点的最大连续子段和( \(rmax\) )
  • 当前区间的最大连续子段和( \(data\) )

为了让以下代码勉强能看,我们令 \(l=\) 左子树,\(r=\) 右子树,\(p=\) 当前节点

\[p.sum=l.sum+r.sum \\ p.lmax=\max(l.lmax,l.sum+r.lmax) \\ p.rmax=\max(r.rmax,r.sum+l.rmax) \\ p.data=\max(l.data,r.data,l.rmax+r.lmax) \]

第一行:求左子树与右子树的和,既为当前区间和

第二行:在左子树的包含左端点的最大连续子段和 和 整个左子树加上右子树包含左端点的最大连续子段和 中取最大值。

daan

第三行:(包含右端点的最大连续子段和同理,不上图了)

第四行:在三个可能的最大连续子段和中选最大值

data

每次这般更新。

查询时也不能就这样:

假设需要查询 \([1,3]\)
查询到了 \([1,2].data\)\([3,3].data\)
可是实际的最大连续子段和在 \([2,3].data\)

那不就尴尬了?所以这里我们不能像以前那样写。

以前:

可以查询左子树 \(\to\) 查询左子树
可以查询右子树 \(\to\) 查询右子树

现在:

不能查询左子树 \(\to\) 查询右子树
不能查询右子树 \(\to\) 查询左子树
都可以查询 \(\to\) 答案在中间,特别计算

Code

#include<cstdio>
typedef long long ll;
const int maxn=2e5+5;
struct Segment_tree{
	ll l,r;
	ll sum,lmax,rmax,data;
}t[maxn<<2];
ll val[maxn];
ll n,m,type,x,y;
void read(ll&x){
	x=0;
	bool f=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch&15);
		ch=getchar();
	}
	if(f)x=-x;
	return;
}
ll max(ll x,ll y){
	return x>y?x:y;
}
void build(ll p,ll l,ll r){
	t[p].l=l,t[p].r=r;
	if(l==r){
		t[p].data=t[p].lmax=t[p].rmax=t[p].sum=val[l];
		return;
	}
	ll mid=l+r>>1;
	ll lt=p<<1;
	ll rt=lt|1;
	build(lt,l,mid);
	build(rt,mid+1,r);
	t[p].sum=t[lt].sum+t[rt].sum;
	t[p].lmax=max(t[lt].lmax,t[lt].sum+t[rt].lmax);
	t[p].rmax=max(t[rt].rmax,t[rt].sum+t[lt].rmax);
	t[p].data=max(t[lt].rmax+t[rt].lmax,max(t[lt].data,t[rt].data));
	return;
}
void Update(ll p,ll x,ll v){
	if(t[p].l==t[p].r){
		t[p].lmax=t[p].rmax=t[p].data=t[p].sum=v;
		return;
	}
	ll mid=t[p].l+t[p].r>>1;
	ll lt=p<<1;
	ll rt=lt|1;
	if(x<=mid)Update(lt,x,v);
	else Update(rt,x,v);
	t[p].sum=t[lt].sum+t[rt].sum;
	t[p].lmax=max(t[lt].lmax,t[lt].sum+t[rt].lmax);
	t[p].rmax=max(t[rt].rmax,t[rt].sum+t[lt].rmax);
	t[p].data=max(max(t[lt].data,t[rt].data),t[lt].rmax+t[rt].lmax);
	return;
}
Segment_tree Query(ll p,ll lr,ll rr){
	if(lr<=t[p].l&&t[p].r<=rr)
		return t[p];
	ll mid=t[p].l+t[p].r>>1;
	ll lt=p<<1;
	ll rt=lt|1;
	if(rr<=mid)return Query(lt,lr,rr);      //不能查右子树就查左子树
	if(lr>mid)return Query(rt,lr,rr);       //不能查左子树就查右子树
	//没有return,两个都可以查
	Segment_tree l,r,ans;
	l=Query(lt,lr,mid);
	r=Query(rt,mid+1,rr);
	ans.sum=l.sum+r.sum;
	ans.lmax=max(l.lmax,l.sum+r.lmax);
	ans.rmax=max(r.rmax,r.sum+l.rmax);
	ans.data=max(l.rmax+r.lmax,max(l.data,r.data));
	return ans;
}
int main(){
	read(n);
	for(int i=1;i<=n;++i)
		read(val[i]);
	build(1,1,n);
	read(m);
	while(m--){
		read(type);read(x);read(y); 
		if(type==1)Update(1,x,y);
		else printf("%lld\n",Query(1,x,y).data);
	}
	return 0;
}

end.

posted @ 2020-11-22 00:46  XSC062  阅读(205)  评论(0编辑  收藏  举报