前言
线段树可以维护一系列区间问题。包括但不限于区间加,区间乘,区间最值等。
本文主要介绍最基础的区间修改区间查询。你可以认为是 模板 线段树 的解析。
本文将持续更新。
例题
给定一个数列,需要支持如下两种操作:
\(1\) \(x\) \(y\) \(k\) 将区间 \([x,y]\) 中每一个数加 \(k\) .
\(2\) \(x\) \(y\) 查询区间 \([x,y]\) 的和.
\(1\le n,m\le 10^5\)
容易发现,若本题区间修改和区间查询操作分离,可以使用前缀和查分解决。但当区间修改和查询操作混在一起时,显然前缀和无法解决问题。
Description
线段树是一棵平衡二叉树,在本题中,每个节点存储了区间 \([l,r]\) 的和。左儿子和右儿子分别存储 \([l,mid]\),\([r,mid]\) 的和。(\(mid = (l+r)\div 2\)).
build
容易发现,线段树的建立可以递归进行。线段树的 root 定义为区间 \([1,n]\) 的区间和。参考代码如下:
build 操作
void build(ll l,ll r,ll p) // l,r 表示当前区间;p表示当前节点编号,参考二叉树的建立。
{
if(l == r) // 到达叶子赋值
{
tree[p] = a[l];
return;
}
ll mid = (l+r) / 2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
tree[p] = tree[p*2] + tree[p*2+1]; // 回溯统计区间和
}
modify
这里引入“懒标记” 的概念(即 lazy tag),可以理解为懒惰修改。
我们在 “堆优化 dijkstra” 中,使用了懒惰修改。由于 priority_queue 不支持随机删除,每次松弛结束的点我们可以打上一个 tag,再次访问时可以直接 continue。
线段树中的 lazy tag 可以类比理解。具体地,对于区间 \([L,R]\) 的修改,朴素做法是不断递归,复杂度太高无法接受。若设当前访问区间为 \([l,r]\) ,当满足 \(l\geq L\) 且 \(r \le R\) 时,也就是区间 \([l,r]\) 是区间 \([L,R]\) 的子集。直接修改即可。同时标记当前区间的所有值均需修改。不必向下递归修改。
反之,当区间 \([l,r]\) 去区间 \([L,R]\) 有交集。我们不得不将区间 \([l,r]\) 拆分,递归求解。此时将区间的 lazy tag 下放。我们只下方一层即可,等后期需要的时候再继续下放。
别忘了将原区间的 lazy tag 删除,因为已经下放到子节点,避免重复修改。
参考代码如下
modify 操作
void pushdown(ll p,ll len) // 下放 lazy tag操作
{
mark[p*2] += mark[p];
mark[p*2+1] += mark[p];
tree[p*2] += (len-len/2)*mark[p];
tree[p*2+1] += (len/2)*mark[p];
mark[p] = 0;
}
void update(ll l,ll r,ll pl,ll pr,ll p,ll d)
{
if(pl > r || pr < l) return; //当前区间与所求区间没有交集,不操作
if(pl >= l && pr <= r) // 当前区间是所求区间的子集,直接修改,标记lazy tag
{
tree[p] += (pr-pl+1) * d;
if(l < r) mark[p] += d;
}
else //当前区间与所求区间有交集,将当前区间拆分,递归修改
{
ll mid = (pl+pr) / 2;
pushdown(p,(pr-pl+1)); // 下放 lazy tag
update(l,r,pl,mid,p*2,d);
update(l,r,mid+1,pr,p*2+1,d);
tree[p] = tree[p*2] + tree[p*2+1]; // 儿子更新完后重新赋值父节点
}
}
query
查询操作类比区间修改。见代码。
query 操作
ll query(ll l,ll r,ll pl,ll pr,ll p)
{
if(pl > r || pr < l) return 0;
if(pl >= l && pr <= r) return tree[p]; // 当前区间是所求区间的子集,直接返回值
else
{
ll mid = (pl+pr) / 2;
return query(l,r,pl,mid,p*2) + query(l,r,mid+1,pr,p*2+1); // 直接递归查询
}
}
以上是区间修改区间查询操作,完整代码如下.
区间修改,区间查询模板
#include <iostream>
#include <cstdio>
#include <algorithm>
#define ll long long
#define N 1000010
using namespace std;
ll tree[N];
ll mark[N];
ll a[N];
ll n,m;
void pushdown(ll p,ll len)
{
mark[p*2] += mark[p];
mark[p*2+1] += mark[p];
tree[p*2] += (len-len/2)*mark[p];
tree[p*2+1] += (len/2)*mark[p];
mark[p] = 0;
}
void build(ll l,ll r,ll p)
{
if(l == r)
{
tree[p] = a[l];
return;
}
ll mid = (l+r) / 2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
tree[p] = tree[p*2] + tree[p*2+1];
}
void update(ll l,ll r,ll pl,ll pr,ll p,ll d)
{
if(pl > r || pr < l) return;
if(pl >= l && pr <= r)
{
tree[p] += (pr-pl+1) * d;
if(l < r) mark[p] += d;
}
else
{
ll mid = (pl+pr) / 2;
pushdown(p,(pr-pl+1));
update(l,r,pl,mid,p*2,d);
update(l,r,mid+1,pr,p*2+1,d);
tree[p] = tree[p*2] + tree[p*2+1];
}
}
ll query(ll l,ll r,ll pl,ll pr,ll p)
{
if(pl > r || pr < l) return 0;
if(pl >= l && pr <= r) return tree[p];
else
{
ll mid = (pl+pr) / 2;
pushdown(p,(pr-pl+1));
return query(l,r,pl,mid,p*2) + query(l,r,mid+1,pr,p*2+1);
}
}
int main()
{
scanf("%lld%lld",&n,&m);
for(ll i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
build(1,n,1);
for(ll i=1;i<=m;i++)
{
ll flg,x,y,z;
scanf("%lld",&flg);
if(flg == 1)
{
scanf("%lld%lld%lld",&x,&y,&z);
update(x,y,1,n,1,z);
}
else
{
scanf("%lld%lld",&x,&y);
printf("%lld\n",query(x,y,1,n,1));
}
}
}
进阶操作
Warning: 在了解进阶操作前,读者需确保理解上文内容.
1. 区间加乘
Description
如题,已知一个数列,你需要进行下面三种操作:
- 将某区间每一个数乘上 \(x\);
- 将某区间每一个数加上 \(x\);
- 求出某区间每一个数的和。
同样使用线段树维护区间值,只是在 pushdown 和 modify 操作中,需要注意优先级。必须确保 “先乘后加”。
(摘自算法学习笔记 线段树Trick)
然后就是常规的线段树写法,区间加和区间乘是两个独立的 lazy tag,显然。
参考代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#define ll long long
#define lc p<<1
#define rc p<<1|1
using namespace std;
const int N = 100005;
struct Node
{
ll v,lz1=1,lz2;
}tree[N<<2];
int n,q,m;
int a[N];
void build(int l,int r,int p)
{
if(l == r)
{
tree[p].v = a[l];
return;
}
int mid = (l+r) / 2 ;
build(l,mid,lc);
build(mid+1,r,rc);
tree[p].v = tree[lc].v + tree[rc].v;
}
void pushdown(int p,int l,int r)
{
int mid=l+r>>1;
ll &lz1=tree[p].lz1,&lz2=tree[p].lz2;
if(lz1!=1){
tree[lc].lz2=tree[lc].lz2*lz1%m;
tree[rc].lz2=tree[rc].lz2*lz1%m;
tree[lc].lz1=tree[lc].lz1*lz1%m;
tree[rc].lz1=tree[rc].lz1*lz1%m;
tree[lc].v=tree[lc].v*lz1%m;
tree[rc].v=tree[rc].v*lz1%m;
lz1=1;
}
if(lz2){
tree[lc].v=(tree[lc].v+(mid-l+1)*lz2)%m;
tree[lc].lz2=(tree[lc].lz2+lz2)%m;
tree[rc].v=(tree[rc].v+(r-mid)*lz2)%m;
tree[rc].lz2=(tree[rc].lz2+lz2)%m;
lz2=0;
}
}
void update1(int l,int r,int pl,int pr,int d,int p)
{
if(pl > r || pr < l) return;
if(pl >= l && pr <= r)
{
tree[p].lz1 = tree[p].lz1 * d % m;
tree[p].lz2 = tree[p].lz2 * d % m;
tree[p].v = (tree[p].v *d) % m;
return;
}
int mid = (pl+pr) >> 1;
pushdown(p,pl,pr);
update1(l,r,pl,mid,d,lc);
update1(l,r,mid+1,pr,d,rc);
tree[p].v = tree[lc].v + tree[rc].v;
}
void update2(int l,int r,int pl,int pr,int d,int p)
{
if(pl > r || pr < l) return;
if(pl >= l && pr <= r)
{
tree[p].lz2 = (tree[p].lz2 + d) % m;
tree[p].v = (tree[p].v + (pr-pl+1)*d) % m;
return;
}
int mid = (pl+pr) >> 1;
pushdown(p,pl,pr);
update2(l,r,pl,mid,d,lc);
update2(l,r,mid+1,pr,d,rc);
tree[p].v = (tree[lc].v + tree[rc].v) % m;
}
long long query(int l,int r,int pl,int pr,int p)
{
if(pl > r || pr < l) return 0;
if(pl >= l && pr <= r)
{
return tree[p].v;
}
int mid = (pl+pr) >> 1;
pushdown(p,pl,pr);
return (query(l,r,pl,mid,lc) + query(l,r,mid+1,pr,rc))%m;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>q>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
build(1,n,1);
while(q--)
{
int flg,x,y,k;
cin>>flg;
if(flg == 1)
{
cin>>x>>y>>k;
update1(x,y,1,n,k,1);
}
if(flg == 2)
{
cin>>x>>y>>k;
update2(x,y,1,n,k,1);
}
else if(flg == 3)
{
cin>>x>>y;
cout<<query(x,y,1,n,1)<<endl;
}
}
return 0;
}