【Trick】标记永久化
1. 理论
线段树使用来维护区间信息的数据结构。回想一下,是否还记得线段树的 pushdown
操作。
在区间修改区间查询中,由于区间修改时信息不一定能传达到位,需要使用 lazy tag 将修改信息打在非叶子节点上(其实可以不用,但时间复杂度错误)。这样一来,当查询的区间在其子区间时,可以把打在当前节点的标记信息下传。便可以正确的维护区间操作。
设想一下,每次查询时都需要将标记下传。那么常数是不是会一丢丢大....
标记永久化横空出世!
2. 原理
标记永久化。顾名思义,不管你如何操作,标记从始至终都不会动。
记 \(val\) 为节点维护的值,\(tag\) 为节点标记。步骤如下:
-
区间修改:除线段树上的修改区间,将包含修改区间的所有节点 \(val\) 修改。\(tag\) 打在线段树上的修改区间。
-
区间查询:除线段树上的查询区间,将包含修改区间的所有节点 \(tag\) 累计。与 \(val\) 一起在线段树上的查询区间统计答案。
以区间加区间查为例:
最开始的线段树。
区间 \([1,4]\) 加 \(1\)。
标记打在 \([1,4]\) 区间,包含修改区间的所有节点(\([1,4]\)节点)加 \((4-1+1)\times 1\)。
区间 \([2,4]\) 加 \(2\)。
标记打在 \([3,4],[2,2]\) 区间,包含修改区间的所有节点(\([1,4],[1,2],[3,4],[2,2]\)节点)加相应的贡献。
查询 \([2,4]\) 区间。
兵分两路 \([1,4]\to [2,2],[1,4]\to [3,4]\)。
-
\([1,4]\to [2,2]\),累加 \(tag\) 贡献 \(1+0=1\),累计答案 \(3+(2-2+1)\times 1=4\);
-
\([1,4]\to [3,4]\),累加 \(tag\) 贡献 \(1\),累计答案 \(6+(4-3+1)\times 1=8\);
答案为 \(4+8=12\)。
3. 正确性证明
每个节点的 \(val\) 还是维护 \([l,r]\) 的贡献,不过每次在修改之后都会实时更新贡献(修改不完整的区间)。
而 \(tag\) 的作用则是修改整块区间,并将 \([l,r]\) 整块区间标记 \(tag\) 的贡献。(修改完整的区间)。
修改不用多说。
查询的时候将包含查询区间的完整区间与不完整区间都统计到了,自然就是要的答案。
关于算重:显然不会,每次标记打在完整区间,修改值在不完整区间。统计也是分开统计,不能也不会弄混。
4. 代码实现
以 P3372 【模板】线段树 1 为例。
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
#define ls p<<1
#define rs p<<1|1
using namespace std;
namespace Read {
template <typename T>
inline void read(T &x) {
x=0;T f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
x*=f;
}
template <typename T, typename... Args>
inline void read(T &t, Args&... args) {
read(t), read(args...);
}
}
using namespace Read;
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e5 + 10;
struct Node {
int l, r, val, add;
} t[N << 2];
int n, m;
void pushup(int p) {
t[p].val = t[ls].val + t[rs].val;
}
void build(int p, int l, int r) {
t[p].l = l, t[p].r = r;
if(l == r) {
read(t[p].val);
return ;
}
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
pushup(p);
}
void upd(int p, int l, int r, int k) {
t[p].val += (min(t[p].r, r) - max(t[p].l, l) + 1) * k;
if(l <= t[p].l && t[p].r <= r) {
t[p].add += k;
return ;
}
int mid = (t[p].l + t[p].r) >> 1;
if(l <= mid) upd(ls, l, r, k);
if(r > mid) upd(rs, l, r, k);
}
int qry(int p, int l, int r, int Tag) {
if(l <= t[p].l && t[p].r <= r) {
return t[p].val + (min(t[p].r, r) - max(t[p].l, l) + 1) * Tag;
}
int mid = (t[p].l + t[p].r) >> 1, ans = 0;
Tag += t[p].add;
if(l <= mid) ans += qry(ls, l, r, Tag);
if(r > mid) ans += qry(rs, l, r, Tag);
return ans;
}
signed main() {
read(n, m);
build(1, 1, n);
while(m--) {
int op, x, y, k;
read(op);
if(op == 1) {
read(x, y, k);
upd(1, x, y, k);
} else {
read(x, y);
cout << qry(1, x, y, 0) << '\n';
}
}
return 0;
}
注意细节:
-
不需要
pushup
,每次区间在递归时就改好了; -
统计答案/贡献时记得去区间交(每次递归到的区间不一定是包含修改/查询区间的区间)。
5. 试用范围
\(tag\) 不需考虑先后顺序以及满足交换律。
其他情况,标记永久化 一律寄掉!!