线段树(单点修改+区间查询)(区间修改+区间查询)

什么是线段树

线段树,是一种二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。它功能强大,支持区间求和,区间最大值,区间修改,单点修改等操作。
线段树的思想和分治思想很相像。
线段树的每一个节点都储存着一段区间[L…R]的信息,其中叶子节点L=R。它的大致思想是:将一段大区间平均地划分成2个小区间,每一个小区间都再平均分成2个更小区间……以此类推,直到每一个区间的L等于R(这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。
这样一来,每一次修改、查询的时间复杂度都只为O(log2n)
O(log2​n)。
但是,可以用线段树维护的问题必须满足区间加法,否则是不可能将大问题划分成子问题来解决的。

什么是区间加法

一个问题满足区间加法,仅当对于区间[L,R]的问题的答案可以由[L,M]和[M+1,R]的答案合并得到。
经典的区间加法问题有:
区间求和
区间最大值

线段树的原理及实现

线段树主要是把一段大区间平均地划分成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在log级别(因为这棵线段树是平衡的)。也就是说,一个[L…R]的区间会被划分成[L…(L+R)/2]和[(L+R)/2+1…R]这两个小区间进行维护,直到L=R。
下图就是一棵[1…10]的线段树的分解过程(相同颜色的节点在同一层)
在这里插入图片描述
**

储存方式

**
通常用的都是堆式储存法,即编号为k的节点的左儿子编号为k∗2,右儿子编号为k∗2+1,父节点编号为k/2,用位运算优化一下,以上的节点编号就变成了k<<1,k<<1∣1,k>>1。通常,每一个线段树上的节点储存的都是这几个变量:区间左边界,区间右边界,区间的答案(这里为区间元素之和,也可以为区间元素最大值等)
下面是线段树的定义: 此处运用了结构体存储每个数节点的区间值,区间左右边界。 也可不用结构体多设两个量也可实现

struct node
{
	int l;//区间左边界
	int r;//区间右边界
	int sum;//区间元素之和
	int lazy;//懒惰标记,下文会提到
}c[N];//N为总节点数

void push_up(int k)//由两个儿子更新节点k的sum
{
	c[k].sum=c[k<<1].sum+c[k<<1|1].sum;   
	//很显然,一段区间的元素和等于它的子区间的元素和   若sum存取的是最大值的话,则此处改为max(左二子sum值,右儿子sum值)
}

初始化(即建树)

void build(int k/*当前树节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/)
{
	c[k].l=l;
	c[k].r=r;//记录一下每个树节点的区间边界
	if(l==r)//递归到叶节点  叶节点代表的就是各number值
	{
		c[k].sum=number[l];//其中number数组为给定的初值
		return;
	}
	int mid=(l+r)/2;//计算左右子节点的边界
	build(k*2,l,mid);//递归到左儿子
	build(k*2+1,mid+1,r);//递归到右儿子
	push_up(k);//记得要用左右子区间的值更新该区间的值
}

单点修改+区间查询

当我们要把下标为k的数字修改(加减乘除、赋值运算等)时,可以直接在根节点往下DFS。如果当前节点的左儿子包含下标为k的数,那么就走到左儿子,否则走到右儿子(右儿子一定包含下标为k的数,因为根节点一定包含这个数,而从根节点往下走,能到达的点也一定包含这个数),直到L=R。这时就走到了只包含k的那个节点,只需要把这个点修改即可(这个点就相当于线段树中唯一只储存着k的信息的节点)。最后记得在回溯的时候把沿途经过的所有的点的值全部修改一下。

void update(int k/*当前树节点的编号*/,int x/*要修改number数组的下标*/,int y/*要把number[x]的数字修改成y*/)
{
	if(c[k].l==c[k].r)
	{
	c[k].sum=y;
	return;
	}//如果当前区间只包含一个元素,那么该元素一定就是我们要修改的。
	//由于该区间的sum一定等于编号为x的数字,所以直接修改sum就可以了。
	int mid=(c[k].l+c[k].r)/2;//计算下一层子区间的左右边界
	if(x<=mid) 
	update(k<<1,x,y);//递归到左儿子
	else 
	update(k<<1|1,x,y);//递归到右儿子
	push_up(k);//记得更新点k的值
}
int query(int k,int l,int r) //当前到了编号为k的节点,查询[l..r]的和
{
	if(l<=a[k].l&&a[k].r<=r) 
		return c[k].sum;//如果当前区间就是真包含于查询区间,那么显然可以直接返回
	int res=0;
	int mid=(c[k].l+c[k].r)>>1;
	if(l<=mid) 
		res+= query(k<<1,l,r);//如果左子区间与查询区间有交集  若是sum存的是最大值 res=max(res,query(k<<1,l,r));
	if(r>mid) 
		res+= query(k<<1|1,l,r);//如果右子区间与查询区间有交集
	return res;
}

区间修改+区间查询(lazy)

区间修改大致分为两步:

1.找到区间中全部都是要修改的点的**线段树中的区间**
2.修改这一段区间的所有点

比如总区间是 [1,10] 现在要让区间[2,8]的值都加上5
将总区间分割[1,5] [6,10] 再分割 [1,3] [4,5] [6,8] [9,10] 我们的目的是让分割的区间满足真包含于要修改的区间
然后直接修改这一分割区间的sum值,然后用lazy标记,然后再进入push_down函数更新该点子树的sum值
如更新[2,8] 则其实是更新了区间 [2,2] [3,3] [4,5] [6,8] 每找到一个这样的真包含区间就加上lazy标记

懒惰标记
标记的含义:本区间已经被更新过了,但是子区间却没有被更新过,被更新的信息是什么(上面例子是加5)

核心就是:我们可以直接修改一个大区间的值,并不需要修改他的子节点,等我们需要单独提出该子节点信息的时候再下传这个懒标记并修改这个子节点

void push_up(int k)//更新节点k的sum
{
	c[k].sum=c[k*2].sum+c[k*2+1].sum;//很显然,一段区间的元素和等于它的子区间的元素和
}
void push_down(int k,int l,int r)
{
	if(c[k].lazy)//如果有lazy标记
	{
		int mid=(l+r)>>1;
		c[k<<1].lazy+=c[k].lazy;//更新左子树lazy值
		c[k<<1|1].lazy+=c[k].lazy;//更新右子树lazy值
		c[k<<1].sum+=c[k].lazy*(mid-l+1);
		c[k<<1|1].sum+=c[k].lazy*(r-mid);
		c[k].lazy=0;
	}
}
void change(int k,int l,int r,int x)
//当前到了编号为k的节点,要把[l..r]区间中的所有元素的值+x
{
	if(l<=c[k].l&&c[k].r<=r)//如果当前节点的区间真包含于要更新的区间内
	{
		c[k].sum+=(c[k].r-c[k].l+1)*x;//更新该区间的sum
		c[k].lazy+=x;//懒惰标记叠加
		return;
	}
	push_down(k,c[k].l,c[k].r)//查询lazy标记,更新子树
	int mid=(c[k].l+c[k].r)>>1;
	if(l<=mid) 
		change(k<<1,l,r,x);//如果左子树与需要更新的区间有交集
	if(r>mid) 
		change(k<<1|1,l,r,x);//如果右子树与需要更新的区间有交集
	push_up(k);//记得更新父节点
}
int query(int k,int l,int r)
//当前到了编号为k的节点,查询[l..r]的和
{
	if(l<=c[k].l&&c[k].r<=r) 
		return c[k].sum;//如果当前区间就是真包含于查询区间,那么显然可以直接返回
	push_down(k,l,r);//每次访问都去检查lazy标记
	int res=0;
	int mid=(c[k].l+c[k].r)>>1;
	if(l<=mid) 
		res+= query(k<<1,l,r);//如果左子区间与查询区间有交集
	if(r>mid) 
		res+= query(k<<1|1,l,r);//如果右子区间与查询区间有交集
	return res;
}

这类线段树区间查询区间修改类题目要灵活运用
附上 poj2777 线段树进阶染色问题

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N=1e5+10;
const int T=30+5;
int n,t,m;
bool vis[T];
struct Node{
	int l;
	int r;
	int col;//存的颜色标号 如果该点存的区间包含多种颜色就为-1,包含一种颜色就为颜色号 没染色就是0 
	int lazy;
}node[N*4];
void push_up(int k)//由两个儿子更新k的col值
{
	if(node[k<<1].col==node[k<<1|1].col)
	node[k].col=node[k<<1].col;  //通过题意改变push_up所更新的值
	else
	node[k].col=-1;
}
void build(int k,int l,int r)
{
	node[k].l=l;
	node[k].r=r;
	if(l==r)
	{
		node[k].col=1;//刚开始染的颜色都为1号
		return;
	}
	int mid=(l+r)>>1;
	build(k<<1,l,mid);
	build(k<<1|1,mid+1,r);
	push_up(k);
}
void push_down(int k)
{
	if(node[k].lazy)
	{
		node[k<<1].lazy=node[k].lazy; //由于更新不是加减乘除而是直接变,所以此处是等于
		node[k<<1].col=node[k].lazy;
		node[k<<1|1].lazy=node[k].lazy;
		node[k<<1|1].col=node[k].lazy;
		node[k].lazy=0;
	}
}
void update(int k,int l,int r,int add)
{
	if(l<=node[k].l&&node[k].r<=r)
	{
		node[k].col=add;//由于更新不是加减乘除而是直接变,所以此处是等于
		node[k].lazy=add;
		return;
	}
	push_down(k);
	int mid=(node[k].l+node[k].r)>>1;
	if(mid>=l)
	 	update(k<<1,l,r,add);
	if(mid<r)
		update(k<<1|1,l,r,add);
	push_up(k);
}
void query(int k,int l,int r)
{
	if(node[k].col>0)
	{
		vis[node[k].col]=true; //出现了哪个色号,就将该vis[色号]=true
		return;
	}
	push_down(k);
	int mid=(node[k].l+node[k].r)>>1;
	if(l<=mid) query(k<<1,l,r);
	if(mid<r) query(k<<1|1,l,r);
}
int main()
{
	scanf("%d%d%d",&n,&t,&m);
	build(1,1,n);
	char k;
	int x,y,z;
	for(int i=1;i<=m;i++)
	{
		scanf("%*c%c",&k);
		if(k=='C')
		{
			scanf("%d%d%d",&x,&y,&z);
			update(1,min(x,y),max(x,y),z);
		}
		else
		{
			scanf("%d%d",&x,&y);
			memset(vis,false,sizeof(vis));
			query(1,min(x,y),max(x,y));
			int ans=0;
			for(int i=1;i<=30;i++)
			{
				if(vis[i]) //通过true的个数判断出有几种颜色
				ans++;
			}
			printf("%d\n",ans);
		}
	} 
	return 0;
} 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

henulmh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值