数据结构基础——线段树

概述

用于维护 区间信息 的数据结构。
O(logn) 实现 区间查询,单点修改,区间修改等操作,信息需满足可加性(包括标记)。
可加性:通过两区间加和得到统计结果,否则,不可能通过分成的子区间来得到[L,R]的统计结果
一个非常典型的例子:
数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和
最大公因数(gcd)——总gcd = gcd( 左区间gcd , 右区间gcd )
那么什么不符合区间可加性呢?
例如:众数——只知道左右区间的众数,没法求总区间的众数
线段树通过把长度不为 1 的区间划分为左右两个区间进行递归求解,并通过合并左右两个区间的信息来得到该区间的信息,下面就是一个简单的线段树
 -
** 以下例子均为维护区间和的线段树**

建树

我们规定,对于一个数组S[MAXN],S[i]表示一个结点,S[i<<1]表示它的左节点,S[i<<1|1]表示右结点

#define maxn 100001
int sum[maxn << 2];
int a[maxn], n;
void pushup(int t)//t为当前结点编号
{
	sum[t] = sum[t << 1] + sum[t << 1 | 1];
}
void build(int l, int r, int t)
{
	if (l == r)//区间长度为1,表示达到了叶子结点
	{
		sum[t] = a[l];//储存数组数字
	}
	int m = (l + r) / 2;
	build(l, m, t << 1);//建立左结点的树
	build(m + 1, r, t << 1 | 1);//建立右结点的树
	pushup(t);//更新结点
}

函数调用

build (1,n,1);

区间查询

如果我们要查询一段区间[L,R]的区间和,首先我们判断所查询的区间是不是在所在区间内,然后以分治的思想,如果左边界比所在区间的中间小,就查询t的左子树和,如果右边界比所在区间的中间大,就查询t的右子树和,递归得到结果

int query(int L, int R, int l, int r, int t) {//查询区间和,L,R表示所求的区间,l,r表示当前节点区间,t表示当前节点编号
	if (L <= l && r <= R) {
		//在区间内,直接返回 
		return sum[t];
	}
	int m = (l + r) /2;
	int s = 0;
	if (L <= m) s += query(L, R, l, m, t << 1);//判断应该从左子树查询还是从右子树查询
	if (R > m) s += query(L, R, m + 1, r, t << 1 | 1);
	return s;
}

函数调用

int ans=query(L,R,1,n,1);

单点修改

void change1(int l, int r, int pos, int val, int t)//在pos的位置修改值
{
	if (l == r) {
		sum[t] = val;
		return;
	}
	int mid = (l + r) / 2;
	if (pos <= mid)change1(l, mid, pos, val, t << 1);
	else change1(mid+1, r, pos, val, t << 1|1);
}
void change2(int l, int r, int pos, int c, int t)//在pos的位置加c
{
	if (l == r) {
		sum[t] +=c ;
		return;
	}
	int mid = (l + r) / 2;
	if(pos<=mid)change2(l, mid, pos, c, t << 1);
	else change2(mid+1, r, pos, c, t << 1|1);
}

函数调用

change1(1,n,pos,val,1);
change2(1,n,pos,c,1);

区间修改

如果我们想让一个区间内所有的值都加上c,该怎么办呢?
最暴力的方法无非是将这个区间内所有的点都加上c,时间复杂度为O(nlog2n),我们不能接受这个复杂度
为了优化时间复杂度,我们引入懒标记

优化

什么是懒标记呢??
对于每个节点另外记录一个值 add[],表示惰性加法标记。
将懒标记类比于欠条,add[t] = x 的含义是:
对于 t 节点的所有子孙节点,他们中的每个元素都要对应加上 x。
即在一次区间修改中,我们只对应修改 logn 个完整覆盖 [l,r] 区间的节点的信息
对于它的子孙们,我们现在用不到它们的信息,因此我们打上一个“欠条”,即懒标记,表示:
“虽然你们现在的区间和(信息)是错的,但我下次访问你们的时候,你们要加上 x。”
这样就可以在不直接访问它们的情况下完成区间加操作,从而降低复杂度。
但下次访问这些节点的子孙节点的时候,我们需要下放懒标记,即真的让它们的区间和加上这些值(修改它们的信息),同时再打上新的懒标。
有点抽象啊,来代码加例子!

void pushdown(int t, int ln, int rn) {//ln,rn为左子树,右子树的数字数量
	if (add[t]) { 
		add[t << 1] += add[t];//左右结点都打上标记
		add[t << 1 | 1] += add[t];
		sum[t << 1] += add[t] * ln;
		sum[t << 1 | 1] += add[t] * rn;
		add[t] = 0;//清除本节点标记 
	}

}

注意其他部分在访问节点时也要加上 pushdown
查询区间代码

int query(int L, int R, int l, int r, int t) {//查询区间和,L,R表示所求的区间,l,r表示当前节点区间,t表示当前节点编号
	if (L <= l && r <= R) {
		//在区间内,直接返回 
		return sum[t];
	}
	int m = (l + r) /2;
	pushdown(t,m-l+1,r-m);
	int s = 0;
	if (L <= m) s += query(L, R, l, m, t << 1);//判断应该从左子树查询还是从右子树查询
	if (R > m) s += query(L, R, m + 1, r, t << 1 | 1);
	return s;

区间修改代码

void change3(int L, int R, int c, int l, int r, int t) {
	if (L <= l && r <= R) {
		sum[t] += c * (r - l + 1);
		add[t] += c;//打标记
		return;
	}
	int m = (l + r) >> 1;
	pushdown(t, m - l + 1, r - m);//下推标记 
	if (L <= m) change3(L, R, c, l, m, t << 1);
	if (R > m) change3(L, R, c, m + 1, r, t << 1 | 1);
	pushup(t);//更新本节点信息 
}

懒标记的其他注意点:
什么类型的修改操作可以用懒标记?
与维护的信息类似,标记的信息也要满足可加性,如例题中的区间加,区间取模就是一个不满足可加性的例子。
常见的支持的区间修改有:区间赋值,区间乘,区间加…
要同时支持多种修改操作怎么办?
例如我要同时支持区间加和区间乘:
首先规定一个标记应用的顺序,这里取先乘后加,即区间内的每个元素都要变成 A*x+B,A为乘法标记的值,B为加法标记的值。
之后考虑标记的合并,推导可得 (A*x+B)*C+D = A*C*x+B*C+D
在考虑标记对区间和信息的影响,区间乘A的影响是:sum[t]*=A,区间加B的影响是:sum[t]+=len*B

下面给出洛谷的两个模板
P3372在这里插入图片描述

#include<iostream>
#include<cstdlib>
#include<cstdio>
using namespace std;
#define ll long long 
#define maxn 100001
ll sum[maxn<< 2],add[maxn<<2];
ll a[maxn], n;
void pushup(ll t)//t为当前结点编号
{
	sum[t] = sum[t << 1] + sum[t << 1 | 1];
}
void pushdown(ll t, ll ln, ll rn) {//ln,rn为左子树,右子树的数字数量
	if (add[t]) {
		add[t << 1] += add[t];//左右结点都打上标记
		add[t << 1 | 1] += add[t];
		sum[t << 1] += add[t] * ln;
		sum[t << 1 | 1] += add[t] * rn;
		add[t] = 0;//清除本节点标记 
	}

}
void build(ll l, ll r, ll t)
{
	if (l == r)//区间长度为1,表示达到了叶子结点
	{
		sum[t] = a[l];//储存数组数字
		return;
	}
	ll m = (l + r) / 2;
	build(l, m, t << 1);//建立左结点的树
	build(m + 1, r, t << 1 | 1);//建立右结点的树
	pushup(t);//更新结点
}
int query(ll L, ll R, ll l,ll r, ll t) {//查询区间和,L,R表示操作区间,l,r表示当前节点区间,t表示当前节点编号
	if (L <= l && r <= R) {
		//在区间内,直接返回 
		return sum[t];
	}
	ll m = (l + r) >> 1;
	pushdown(t, m - l + 1, r - m);
	ll s = 0;
	if (L <= m) s += query(L, R, l, m, t << 1);
	if (R > m) s += query(L, R, m + 1, r, t << 1 | 1);
	return s;
}

void change3(ll L, ll R, ll c, ll l, ll r, ll t) {
	if (L <= l && r <= R) {
		sum[t] += c * (r - l + 1);
		add[t] += c;//打标记
		return;
	}
	ll m = (l + r) >> 1;
	pushdown(t, m - l + 1, r - m);//下推标记 
	if (L <= m) change3(L, R, c, l, m, t << 1);
	if (R > m) change3(L, R, c, m + 1, r, t << 1 | 1);
	pushup(t);//更新本节点信息 
}
int main()
{
	ll n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
	}
	build(1, n, 1);
	while (m--)
	{
		int temp;
		cin >> temp;
		ll l, r, k;
		if (temp == 1)
		{
			cin >> l >> r >> k;
			change3(l, r, k, 1, n, 1);
		}
		if (temp == 2)
		{
			cin >> l >> r;
			int ans = query(l, r, 1, n, 1);
			cout << ans<<endl;
		}
	}
	return 0;
}

P3373
在这里插入图片描述


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

青云遮夜雨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值