【Trick】标记永久化

1. 理论

线段树使用来维护区间信息的数据结构。回想一下,是否还记得线段树的 pushdown 操作。

在区间修改区间查询中,由于区间修改时信息不一定能传达到位,需要使用 lazy tag 将修改信息打在非叶子节点上(其实可以不用,但时间复杂度错误)。这样一来,当查询的区间在其子区间时,可以把打在当前节点的标记信息下传。便可以正确的维护区间操作。

设想一下,每次查询时都需要将标记下传。那么常数是不是会一丢丢大....

标记永久化横空出世!

2. 原理

标记永久化。顾名思义,不管你如何操作,标记从始至终都不会动。

\(val\) 为节点维护的值,\(tag\) 为节点标记。步骤如下:

  1. 区间修改:除线段树上的修改区间,将包含修改区间的所有节点 \(val\) 修改。\(tag\) 打在线段树上的修改区间。

  2. 区间查询:除线段树上的查询区间,将包含修改区间的所有节点 \(tag\) 累计。与 \(val\) 一起在线段树上的查询区间统计答案。

以区间加区间查为例:

image

最开始的线段树。

区间 \([1,4]\)\(1\)

image

标记打在 \([1,4]\) 区间,包含修改区间的所有节点(\([1,4]\)节点)加 \((4-1+1)\times 1\)

区间 \([2,4]\)\(2\)

image

标记打在 \([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. \([1,4]\to [2,2]\),累加 \(tag\) 贡献 \(1+0=1\),累计答案 \(3+(2-2+1)\times 1=4\)

  2. \([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;
}

注意细节:

  1. 不需要 pushup,每次区间在递归时就改好了;

  2. 统计答案/贡献时记得去区间交(每次递归到的区间不一定是包含修改/查询区间的区间)。

5. 试用范围

\(tag\) 不需考虑先后顺序以及满足交换律

其他情况,标记永久化 一律寄掉!!

posted @ 2024-02-09 01:38  Daniel_yzy  阅读(120)  评论(0编辑  收藏  举报
Title