首页 > 其他分享 >线段树

线段树

时间:2023-02-19 21:24:23浏览次数:42  
标签:左子 int 线段 右子 修改 区间 节点

前言

线段树经常用来维护区间信息,支持单点修改、区间修改、单点查询、区间查询等操作。其中单点修改/查询其实可以类似地看成区间长度为 \(1\) 的区间。而上面的操作时间复杂度均为 \(O(\log n)\).

基本结构和建树

线段树是采用递归的思想来维护信息的,将每个长度不为 \(1\) 的区间划分为左右两个区间来递归求解,所以我们可以用二叉树来存储线段树,换言之,线段树就是一棵二叉树。而因为线段树是一棵二叉树,所以我们一般需要开 \(4\) 倍空间来存储数据。

例如有一个序列 \(A=\{1,3,4,2,5,6\}\),而我们要维护的信息是区间和,那么我们用线段树来存储就是类似于这样一个结构。

那么我们就可以考虑递归建树,从根节点开始,每次将结点所存储的区间划分为两个子区间,然后向下建树。

int a[N];//表示需存储的序列 

struct node {
	int l,r;//该节点的数据区间
	int val;//该节点的值 
}tr[N<<2];//N<<2等同于N*4

void push_up(int x) {
	tr[x].val=tr[x<<1].val+tr[x<<1|1].val;//x<<1等同于x*2 x<<1|1等同于x*2+1
}

void build(int l,int r,int x) {//l,r表示当前区间,而x表示当前区间所属于的结点编号 
	tr[x].l=l;tr[x].r=r;
	if(l==r) {//如果区间长度为1,直接赋值 
		tr[x].val=a[l];
		return;
	} 
	int mid=(l+((r-l)>>1));//l+((r-l)>>1)等同于(l+r)/2
	build(l,mid,x<<1);//x<<1等同于x*2 
	build(mid+1,r,x<<1|1);//x<<1|1等同于x*2+1
	push_up(x);//向上合并 
} 

单点修改

单点修改时我们就直接从根节点向下找到长度为 \(1\) 且包含这个位置的结点。例如上面的线段树,我们要将 \([3,3]\) 的值修改为 \(8\),那么我们就先从根节点开始找,一直递推下去直到找到区间 \([3,3]\) 的节点,然后修改该节点的值,接着再递归合并即可。

以上面这棵线段树为例,具体过程如下:

  1. 从根节点开始找,区间为 \([1,6]\),可发现左子树包含区间 \([1,3]\),于是向左子树找。

  2. 当前区间为 \([1,3]\),可见右子树包含区间,于是向右子树找。

  3. 当前区间为 \([3,3]\),已找到该区间,修改值。

  4. 向上递归为根节点,计算根节点的值。

  5. 继续递归到整颗线段树的根节点,然后修改值。

void update(int p,int k,int x) {//将a[p]的值更改为k,x表示当前查找结点 
	if(tr[x].l==p&&tr[x].r==p) { 
		tr[x].val=k;
		return;
	}
	int mid=(l+((r-l)>>1));
	if(p<=mid) update(p,k,x<<1);
	else update(p,k,x<<1|1);
	push_up(x);
}

单点查询

那么单点查询类似于单点修改,我们只要找到包含该结点的区间,且区间长度为 \(1\),然后直接返回该节点的值即可。

区间查询

当我们要求 \([l,r]\) 的区间和时,我们一样先从根节点开始找。先判断该区间是否完全包含当前节点的区间,如果是的话,我们直接返回该区间的值,如果不是,那么如果与左子树有交集,那么我们就向左子树递推,如果与右子树有交集,那么我们就向右子树递推。

我们以查询区间 \([3,6]\) 的区间和为例:

  1. 先从根节点开始查找,可见根节点的区间 \([1,6]\) 不被查询区间包含,那么我们就判断左子树,左子树与查询区间有交集,所以我们向左子树递推。

  2. 当前区间 \([1,3]\) 不被查询区间包含,那么我们就可以发现查询区间与左子树没有交集,但是与右子树有交集,所以我们向右子树递推,可以发现右子树区间 \([3,3]\) 被查询区间完全包含,那么我们就获取该区间的区间和,累加到答案中。至此,根节点的左子树就查找完毕。

  3. 我们继续判断根节点的右子树,可以发现根节点的右子树与查询区间是被完全包含的关系,所以我们直接返回右子树的区间和,累加到答案中。

  4. 我们将左右子树查找出来的答案合并起来,便是区间 \([3,6]\) 的和。

int query(int l,int r,int x) {//查询区间[l,r]的和,当前查找节点为x 
	if(l<=tr[x].l&&tr[x].r<=r) return tr[x].val;
	int ans=0;
	int mid=(tr[x].l+((tr[x].r-tr[x].l)>>1));
	if(l<=mid) ans+=query(l,r,x<<1);
	if(r>mid) ans+=query(l,r,x<<1|1);
	return ans; 
}

区间修改

我们如果要进行区间修改,很容易想到的做法就是像单点修改一样,将区间的每一个点都进行修改,那么这样就和暴力做法相差无几了。所以为了保证时间复杂度,我们就要引入懒标记。

顾名思义,懒标记就是一个标记,作用是来延迟操作。当我们需要时,我们在进行操作,不需要时只需要记录有这次操作的存在即可。这样可以大幅降低不必要的操作次数。

详细地说,就是我们只更改当前区间的值,而不更新区间包含的子区间的值,只是打上标记,当我们需要用到子区间时,再更新子区间的值。

我们在进行区间修改时遵循着以下的基本步骤:判断当前区间是否被完全包含于修改区间。如果包含,则直接修改当前结点的值,同时将该节点的懒标记进行更新,直接回溯。如果不包含,则类似于区间查询,向满足条件的左右子树递推。重复以上操作。要注意的是,在修改区间的值时,应当是修改的值与区间长度的乘积。

我们以将区间 \([3,5]\) 的值全部更改为 \(5\) 为例:

  1. 从根节点开始找,当前区间 \([1,6]\) 不被完全包含于修改区间中,经过判断发现,该左子树与修改区间有交集,于是向左子树递推。

  2. 可以发现当前区间 \([1,3]\) 不被完全包含于修改区间中,经过判断发现,该右子树与修改区间有交集,于是向右子树递推。可以发现右子树被完全包含于修改区间,所以我们修改当前节点的值为 \(5\times (3-3+1)=5\),并将当前节点懒标记的值修改为 \(5\).这样根节点的左子树就修改完毕。

  3. 可以发现根节点的右子树与修改区间有交集,于是我们向右子树递推。右子树并不被完全包含于修改区间,所以经过比较发现,当前区间的左子树与修改区间有交集,经过进一步比较发现,当前区间的左子树是被完全包含的关系,于是我们将当前区间的左子树的区间值修改为 \(5 \times (5-4+1)=10\),并将当前区间的懒标记的值修改为 \(5\).

  4. 这样区间修改就完毕了。

那么类似地,区间查询也只要加上一步下放标记即可。

struct node {
	int l,r;//该节点的数据区间
	int val;//该节点的值 
	int lazy;//懒标记 
}tr[N<<2];//N<<2等同于N*4

void push_down(int x) {
	if(tr[x].lazy) {
		tr[x<<1].lazy=tr[x<<1|1].lazy=tr[x].lazy;
		tr[x<<1].val=tr[x].lazy*(tr[x<<1].r-tr[x<<1].l+1);
		tr[x<<1|1].val=tr[x].lazy*(tr[x<<1|1].r-tr[x<<1|1].l+1);
		tr[x].lazy=0;
	}
}

void update(int l,int r,int k,int x) {//将区间[l,r]的值更改为k,x表示当前查找结点 
	if(l<=tr[x].l&&tr[x].r<=r) {
		tr[x].val=k*(tr[x].r-tr[x].l+1);
		tr[x].lazy=k;
		return;
	} 
	push_down(x);
	int mid=(tr[x].l+((tr[x].r-tr[x].l)>>1));
	if(l<=mid) update(l,r,k,x<<1);
	if(r>mid) update(l,r,k,x<<1|1);
	push_up(x);
}

int query(int l,int r,int x) {//查询区间[l,r]的和,当前查找节点为x 
	if(l<=tr[x].l&&tr[x].r<=r) return tr[x].val;
	int ans=0;
	push_down(x);
	int mid=(tr[x].l+((tr[x].r-tr[x].l)>>1));
	if(l<=mid) ans+=query(l,r,x<<1);
	if(r>mid) ans+=query(l,r,x<<1|1);
	return ans; 
}

总结

线段树可以维护区间和,区间乘积,区间最值等,其每次操作时间复杂度为 \(O(\log n)\).

例1:P3372

很明显,我们可以用一棵线段树来维护区间和,需要的操作有区间修改和区间查询。因为有 \(m\) 次操作,每次操作时间复杂度为 \(O(\log n)\),所以时间复杂度为 \(O(m \log n)\).

#include <cstdio>
#include <iostream>

using namespace std;

const int N=400005;

#define int long long

int n,m;

struct node {
	int l,r;
	int lazy,val;
}tr[N<<2];

int a[N];

void push_up(int p) {
	tr[p].val=tr[p<<1].val+tr[p<<1|1].val;
}

void build(int l,int r,int p) {
	tr[p].l=l;tr[p].r=r;
	if(l==r) {
		tr[p].val=a[l];
		return;
	}
	int mid=l+((r-l)>>1);
	build(l,mid,p<<1);
	build(mid+1,r,p<<1|1);
	push_up(p);
}

void push_down(int x) {
	if(tr[x].lazy) {
		tr[x<<1].lazy+=tr[x].lazy;
		tr[x<<1|1].lazy+=tr[x].lazy;
		tr[x<<1].val+=tr[x].lazy*(tr[x<<1].r-tr[x<<1].l+1);
		tr[x<<1|1].val+=tr[x].lazy*(tr[x<<1|1].r-tr[x<<1|1].l+1);
		tr[x].lazy=0;
	}
}

void update(int l,int r,int k,int x) { 
	if(l<=tr[x].l&&tr[x].r<=r) {
		tr[x].val+=k*(tr[x].r-tr[x].l+1);
		tr[x].lazy+=k;
		return;
	} 
	push_down(x);
	int mid=(tr[x].l+((tr[x].r-tr[x].l)>>1));
	if(l<=mid) update(l,r,k,x<<1);
	if(r>mid) update(l,r,k,x<<1|1);
	push_up(x);
}

int query(int l,int r,int x) {
	if(l<=tr[x].l&&tr[x].r<=r) return tr[x].val;
	int ans=0;
	push_down(x);
	int mid=(tr[x].l+((tr[x].r-tr[x].l)>>1));
	if(l<=mid) ans+=query(l,r,x<<1);
	if(r>mid) ans+=query(l,r,x<<1|1);
	return ans; 
}

signed main() {
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	build(1,n,1);
	int opt,x,y,k;
	while(m--) {
		scanf("%lld",&opt);
		if(opt==1) {
			scanf("%lld%lld%lld",&x,&y,&k);
			update(x,y,k,1);
		}
		else {
			scanf("%lld%lld",&x,&y);
			printf("%lld\n",query(x,y,1));
		}
	}
	return 0;
}

例2:P3373

这道题需要区间加和区间乘两种操作,那么只用一个懒标记是不行的,所以我们就需要两个懒标记,一个来记录加,一个来记录乘。

那么就有个问题,我们在 push_down 时要先加后乘,还是先乘后加。

我们注意到,如果先加后乘,那么结果就是 (val+add)*mul=val*mul+add*mul,而如果先乘后加,那么结果就是 val*mul+add,所以我们应当先乘后加,并且在下放标记时,懒标记中的 add 也应当相应的做乘法操作。

#include <cstdio>
#include <iostream>

using namespace std;

const int N=1e5+10;

struct node {
	int l,r;
	long long val,add,mul;
}tr[N<<2];

int n,m,mod;
int a[N];

void push_up(int p) {
	tr[p].val=(tr[p<<1].val+tr[p<<1|1].val)%mod;
}

void push_down(int p) {
	tr[p<<1].val=((tr[p<<1].val*tr[p].mul)%mod+((tr[p<<1].r-tr[p<<1].l+1)*tr[p].add)%mod)%mod;
	tr[p<<1|1].val=((tr[p<<1|1].val*tr[p].mul)%mod+((tr[p<<1|1].r-tr[p<<1|1].l+1)*tr[p].add)%mod)%mod;
	tr[p<<1].mul=(tr[p].mul*tr[p<<1].mul)%mod;
	tr[p<<1|1].mul=(tr[p].mul*tr[p<<1|1].mul)%mod;
	tr[p<<1].add=(tr[p].mul*tr[p<<1].add+tr[p].add)%mod;
	tr[p<<1|1].add=(tr[p].mul*tr[p<<1|1].add+tr[p].add)%mod;
	tr[p].add=0;
	tr[p].mul=1;
}

void build(int p,int l,int r) {
	tr[p].l=l;
	tr[p].r=r;
	tr[p].mul=1;
	if(l==r) {
		tr[p].val=a[l]%mod;
//		printf("%d %d %lld\n",l,r,tr[p].val);
		return;
	}
	int mid=l+((r-l)>>1);
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	push_up(p);
//	printf("%d %d %lld\n",l,r,tr[p].val);
} 

void change_mul(int p,int l,int r,int k) {
	if(l<=tr[p].l&&tr[p].r<=r) {
		tr[p].add=(tr[p].add*k)%mod;
		tr[p].mul=(tr[p].mul*k)%mod;
		tr[p].val=(tr[p].val*k)%mod;
		return;
	}
	push_down(p);
	int mid=tr[p].l+((tr[p].r-tr[p].l)>>1);
	if(l<=mid) change_mul(p<<1,l,r,k);
	if(r>mid) change_mul(p<<1|1,l,r,k);
	push_up(p);
}

void change_add(int p,int l,int r,int k) {
//	printf("%d %d\n",tr[p].l,tr[p].r);
	if(l<=tr[p].l&&tr[p].r<=r) {
		tr[p].add=(tr[p].add+k)%mod;
		tr[p].val=(tr[p].val+k*(tr[p].r-tr[p].l+1))%mod;
		return;
	}
	push_down(p);
	int mid=tr[p].l+((tr[p].r-tr[p].l)>>1);
	if(l<=mid) change_add(p<<1,l,r,k);
	if(r>mid) change_add(p<<1|1,l,r,k);
	push_up(p);
}

long long query(int p,int l,int r) {
	if(l<=tr[p].l&&tr[p].r<=r) return tr[p].val;
	push_down(p);
	long long res=0;
	int mid=tr[p].l+((tr[p].r-tr[p].l)>>1);
	if(l<=mid) res=(res+query(p<<1,l,r))%mod;
	if(r>mid) res=(res+query(p<<1|1,l,r))%mod;
	return res;
}

int main() {
	scanf("%d%d%d",&n,&m,&mod);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	build(1,1,n);
//	printf("%lld\n",query(1,1,n));
//	for(int i=1;i<=n;i++) printf("%lld ",query(1,i,i));
//	puts("");
	int opt,x,y,k;
	while(m--) {
		scanf("%d%d%d",&opt,&x,&y);
		if(opt==1) {
			scanf("%d",&k);
			change_mul(1,x,y,k);
		} else if(opt==2) {
			scanf("%d",&k);
			change_add(1,x,y,k);
		} else if(opt==3) {
			printf("%lld\n",query(1,x,y));
		}
	}
	return 0;
}

标签:左子,int,线段,右子,修改,区间,节点
From: https://www.cnblogs.com/Mr-Lin-081122/p/17135617.html

相关文章

  • 线段树
    权值线段树线段树维护的区间是一个值域范围,而不是一段下标的区间。P1637三元上升子序列我们可以考虑中间的数字\(x\),统计前面比\(x\)小的数字个数\(a_1\),统计后......
  • ACwing 区间最大公约数题解 线段树(附证明)
    算进区间最大公因数单点线段树 https://www.acwing.com/problem/content/247/题目:给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:Clrd,表......
  • 线段树以及其高级用法
    1.线段树2.动态开点及标记永久化3.线段树的分裂和合并4.李超线段树5.zkw线段树6.树套树7.线段树分治8.猫树......
  • Educational Codeforces Round 102 (Rated for Div. 2)D(线段树求贡献)
    EducationalCodeforcesRound102(RatedforDiv.2)D(线段树求贡献)D.Program题目大意:最初x为0,给定一个长度为n的操作序列,共有两种操作:-,x-=1;+,x+=1;有m次询......
  • Codeforces Round #442 (Div. 2)E. Danil and a Part-time Job 线段树+lazytag
    题意:一颗有根树,树上每一个节点有一个灯,要支持两种操作第一种操作是统计一颗子树内开着的灯个数。第二种操作是将一个子树内的所有灯状态改变(开灯->关灯,关灯->开灯)。解......
  • CF1567E Non-Decreasing Dilemma 题解 线段树
    题目链接:http://codeforces.com/problemset/problem/1567/E题目大意:有一个长度为\(n\)的数列\(a\),你需要对其进行\(q\)次操作,操作有两种类型,按如下格式给出:1xy:......
  • 卡常科技:树状数组做线段树 1
    树状数组能维护的东西:单点修改,查前缀和。树状数组1直接朴素前缀和,树状数组2就差分一下。对于线段树1的操作,不好用一个树状数组维护。首先得把区间加给变成单点加......
  • 线段树
    线段树(SegmentTree)线段树是主要用于维护区间信息(要求满足结合律)的数据结构。与树状数组相比,线段树可以在\(O(\logN)\)的时间复杂度内实现单点修改、区间修改、区间查询......
  • 线段树查询i到j最长增加子串和序列
    基础篇最长增加子数组-楠030416-博客园(cnblogs.com)增加线段树子串#include<bits/stdc++.h>usingnamespacestd;//最长连续增加子串inta[100],dp[100],tr......
  • 线段树模板(cpp)
    这个线段树模板修改起来较为简单轻松,结构也比较简单明了//线段树的信息constintN=2e5+10,mod=1e9+7;inta[N];structinfo//存储线段树的值{ intsize;......