概述
用于维护 区间信息 的数据结构。
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