线段树

线段树

概念

线段树是用来解决区间修改,区间查询的问题,它的修改与求和都是\(O(log_2n)\)的,效率非常高。它与前缀和的区别是能够修改数组中的元素。

基本思想

线段树是一颗二叉树,其每一个节点都对应序列的一段区间。如下图所示:

graph TB; A[[1 - 8]] B[[1 - 4]] C[[5 - 8]] D[[1 - 2]] E[[3 - 4]] F[[5 - 6]] G[[7 - 8]] A-->B A-->C B-->D B-->E C-->F C-->G D-->1 D-->2 E-->3 E-->4 F-->5 F-->6 G-->7 G-->8

可以发现根节点对应的是整个区间\([1,8]\)。若一个节点对应的区间为\([l,r]\),当\(l=r\)时,该节点为叶子节点。否则令\(mid=\cfrac{l+r}{2}\),其左右儿子分别是\([l,mid]\)\([mid+1,r]\)。显然,二叉树高度是\(log_2n\)级别的,所以时间复杂度也是\(O(log_2n)\)

算法实现

建树

由于线段树是一个二叉树结构,我们采用递归来建树。我们令\(build(k,l,r)\)表示当前构建编号为\(k\)\([l,r]\)区间。若\(l=r\),则该节点为叶子节点,赋值为初始值。否则,我们我们构造它的左右子树 \(build(k\times 2,l,mid)\)\(build(k\times 2+1,mid+1,r)\),并合并两个子节点的答案。

区间询问

如下图,若要询问区间\([1,7]\),我们需要知道\([1,4]\)\([5,6]\)\([7,7]\)三个节点的信息:

graph TB; A[1 - 8] B[# 1 - 4 #] C[5 - 8] D[1 - 2] E[3 - 4] F[# 5 - 6 #] G[7 - 8] H[# 7 #] A-->B A-->C B-->D B-->E C-->F C-->G D-->1 D-->2 E-->3 E-->4 F-->5 F-->6 G-->H G-->8

令待查询区间为\([x,y]\)当前访问到了编号为\(k\)的区间\([l,r]\),这时有三种情况:

  1. 若待区间\(k\)与待查询区间完全无交集(\(y < l\)\(x>r\)),该节点对答案没有贡献。
  2. 若待区间\(k\)是带查询区间的子集(\(x<=l\)\(r<=y\)),直接返回该节点的答案。
  3. 除上述两种情况外,继续递归左右子树统计答案。

单点修改

graph TB; A[# 1 - 8 #] B[# 1 - 4 #] C[5 - 8] D[# 1 - 2 #] E[3 - 4] F[5 - 6] G[7 - 8] H[# 1 #] A-->B A-->C B-->D B-->E C-->F C-->G D-->H D-->2 E-->3 E-->4 F-->5 F-->6 G-->7 G-->8

若要修改节点\([1,1]\)的值,我们只需要修改包含它的节点(它和它的所有祖先节点)\([1,1]\)\([1,2]\)\([1,4]\)\([1,8]\)

此时也有三种情况:

  1. 若待区间\(k\)与待查询区间完全无交集(\(y < l\)\(x>r\)),则直接返回。
  2. 若该节点为叶子节点且就是将要修改的点,修改并返回。
  3. 除上述两种情况外,继续递归左右子树并合并答案。

延迟标记(\(Lazy-Tag\)

我们很多时候解决的不仅是单点修改,区间查询,而是区间修改,区间查询

假设区间修改操作是将一个区间同时加上一个数。我们在节点\(k\)上维护一个值\(tag_k\),表示\(k\)这个节点中所有数都加上了\(tag_k\)。此时,答案就是路径上包含这个点的节点的\(tag_i\)之和

但这样是不可行的,应为我们需要修改所有可能影响这个区间的节点,最坏复杂度可能达到\(O(n)\)

我们将单点查询,区间修改区间查询,单点修改结合起来,及维护每个节点的和(\(sum_k\)),又维护加和标记(\(tag_k\))。我们称这样的标记叫做延迟标记(\(Lazy-Tag\)

标记下传

执行修改操作时,我们先找到对应节点\(k\),并修改\(tag_k\),并更新\(sum_k\)。但我们无法直接修改子节点的区间和,因为子节点数量很多,时间复杂度高。我们在处理\(k\)区间时完全没有必要去更新它的子节点,我们只需要将\(tag_k\)的值向下传递并清空,在需要用到子节点时在进行计算。

代码实现

namespace SGT {
  const int N=1e5+5;
  int n, src[N], seg[N<<2], lz[N<<2];
  #define lc (p<<1)
  #define rc (p<<1|1)
  #define mid ((l+r)>>1)

  void build(int p=1, int l=1, int r=n) { // 建树
      if(l == r) { seg[p] = src[l]; return; }
      build(lc,l,mid); build(rc,mid+1,r);
      seg[p] = seg[lc] + seg[rc];
  }

  void push_down(int p, int l, int r) { // 下传标记
      if(!lz[p]) return;
      seg[lc] += lz[p]*(mid-l+1);
      seg[rc] += lz[p]*(r-mid);
      lz[lc] += lz[p], lz[rc] += lz[p];
      lz[p]=0;
  }

  void update(int L, int R, int v, int p=1, int l=1, int r=n) { // 区间更新
      if(L<=l && r<=R) { seg[p] += v*(r-l+1); lz[p] += v; return; }
      push_down(p,l,r);
      if(L<=mid) update(L,R,v,lc,l,mid);
      if(R>mid) update(L,R,v,rc,mid+1,r);
      seg[p] = seg[lc] + seg[rc];
  }

  int query(int L, int R, int p=1, int l=1, int r=n) { // 区间查询
      if(L<=l && r<=R) return seg[p];
      push_down(p,l,r); int s=0;
      if(L<=mid) s += query(L,R,lc,l,mid);
      if(R>mid) s += query(L,R,rc,mid+1,r);
      return s;
  }

  void init(int _n, int a[]) { // 初始化
      n=_n;
      for(int i=1;i<=n;++i) src[i]=a[i];
      fill(lz,lz+(n<<2)+1,0);
      build();
  }
};
posted @ 2025-03-10 22:49  nightmare_lhh  阅读(22)  评论(0)    收藏  举报