线段树复习笔记

线段树是一种高效的数据结构,常用于处理区间问题,如区间和、区间最大值等。本文详细介绍了线段树的建树、单点修改、无更新查询和区间修改操作,并提供了懒惰标记的实现方式,适用于区间修改查询。此外,还讨论了线段树在处理不同区间操作时的时间复杂度。参考代码展示了线段树在实际问题中的应用。
摘要由CSDN通过智能技术生成

线段树(Segment Tree)复习笔记

By Chesium 2021/10/21

  • 线段树能够维护满足以下条件的区间问题:
    f ( l , r ) = g [ f ( l , m ) , f ( m + 1 , r ) ] f(l,r)=g[f(l,m),f(m+1,r)] f(l,r)=g[f(l,m),f(m+1,r)]
    即区间 [ l , r ] [l,r] [l,r] 的答案可以通过合并 [ l , m ] [l,m] [l,m] [ m + 1 , r ] [m+1,r] [m+1,r] 的答案得到。
    如这些可以:区间和、区间最大值。
    如这些不行:区间众数、区间最长不下降子序列

  • 数组 d d d 长度开至 4 n 4n 4n 即可。

  • 建树:递归思想

    • 时间复杂度: O ( n ) \mathrm{O}(n) O(n)
    • 参数:对应数组闭区间 [ s , t ] [s,t] [s,t],以及当前处理的 d d d 数组索引 p p p p p p 一开始是 1 1 1,不能为 0 0 0
    1. s = t s=t s=t,就不用再细分了,直接令 d [ p ] = a [ s ] d[p]=a[s] d[p]=a[s],返回。
    2. 否则,则二分处理:令 m = s + ( ( t − s ) >  ⁣ ⁣ > 1 ) m=s+((t-s)>\!\!>1) m=s+((ts)>>1) ,即为数组中间索引,将 [ s , t ] [s,t] [s,t] 分成 [ s , m ] [s,m] [s,m] [ m + 1 , t ] [m+1,t] [m+1,t] 两部分。分别建树,两者对应的索引 p p p 分别为 2 p 2p 2p 2 p + 1 2p+1 2p+1
    3. 建完树后,我们便把上述两区间合并,形成当前正在建立的区间的值。
  • 单点修改:DFS

    • 时间复杂度: O ( log ⁡ n ) \mathrm{O}(\log n) O(logn)
    • 参数:需更新元素索引 k k k,修改值 c c c,当前节点对应区间 [ s , t ] [s,t] [s,t],当前节点编号( d d d 数组索引) p p p
    1. s = t s=t s=t,则当前区间中只有一个元素,肯定是我们要改的,直接修改 d [ p ] d[p] d[p],返回。
    2. 计算出中位索引 m m m
    3. x ≤ m x\leq m xm,则说明修改值在 [ s , m ] [s,m] [s,m] 中,否则在 [ m + 1 , t ] [m+1,t] [m+1,t] 中,递归更新。
    4. 回溯时更新自己的值 d [ p ] d[p] d[p]
  • 无更新查询:递归思想

    • 时间复杂度: O ( log ⁡ n ) \mathrm{O}(\log n) O(logn)
    • 参数:查询区间 [ l , r ] [l,r] [l,r],当前节点对应区间 [ s , t ] [s,t] [s,t],当前节点编号( d d d 数组索引) p p p
      • 可以理解为求查询区间与当前节点区间交集的对应值
    1. l ≤ s   ∧   t ≤ r l\leq s\ \wedge\ t\leq r ls  tr,即 [ s , t ] ⊆ [ l , r ] [s,t]\subseteq[l,r] [s,t][l,r],当前区间必是最终查询值的组成部分,称为查询区间的一个极大区间,直接返回自己的值 d [ p ] d[p] d[p]
    2. 否则,二分为上面讲过的两区间 [ s , m ] [s,m] [s,m] [ m + 1 , t ] [m+1,t] [m+1,t],不改变 [ l , r ] [l,r] [l,r],进行查询。
      • l ≤ m l\leq m lm,则 [ s , m ] ∩ [ l , r ] ≠ ∅ [s,m]\cap[l,r]\neq\varnothing [s,m][l,r]= ,需要考虑,查询 [ s , m ] [s,m] [s,m]
      • r > m r>m r>m,则 [ m + 1 , t ] ∩ [ l , r ] ≠ ∅ [m+1,t]\cap[l,r]\neq\varnothing [m+1,t][l,r]= ,需要考虑,查询 [ m + 1 , t ] [m+1,t] [m+1,t]
    3. 回溯时合并区间即可。
  • 区间修改:懒惰标记

    • 时间复杂度: O ( log ⁡ n ) \mathrm{O}(\log n) O(logn)
    • 目的:通过延迟对节点信息的更改,从而减少可能不必要的操作次数。
    • 参数:修改区间 [ l , r ] [l,r] [l,r],修改值 c c c,当前节点对应区间 [ s , t ] [s,t] [s,t],当前节点编号( d d d 数组索引) p p p
    1. l ≤ s   ∧   t ≤ r l\leq s\ \wedge\ t\leq r ls  tr,即 [ s , t ] ⊆ [ l , r ] [s,t]\subseteq[l,r] [s,t][l,r],当前区间是修改区间的子集,肯定要改:修改 d [ p ] d[p] d[p],添加标记 b [ p ] b[p] b[p]。(更新标记,某些操作为往上叠加(如增加),有些是覆盖(如赋值)),返回。
    2. 若有标记存在则进行标记下传(防止父区间的懒惰标记下传时覆盖子区间的懒惰标记)
    3. 类似上述的无更新查询,分别考虑二分后的两区间是否需要进行操作。
    4. 回溯时记得合并区间!
  • 标记下传

    • 时间复杂度: O ( 1 ) \mathrm{O}(1) O(1)
    • 参数:当前节点对应区间 [ s , t ] [s,t] [s,t],当前节点编号( d d d 数组索引) p p p
    1. 若无标记,则直接返回。
    2. s = t s=t s=t,则标记无作用,也没有区间用于下传,直接返回。
    3. 求出 m m m ,根据区间范围修改 d [ 2 p ] d[2p] d[2p] d [ 2 p + 1 ] d[2p+1] d[2p+1] 的值
    4. 更新子区间的懒惰标记 b [ 2 p ] b[2p] b[2p] b [ 2 p + 1 ] b[2p+1] b[2p+1]
    5. 清除自身的懒惰标记。
  • 带修改区间查询

    • 时间复杂度: O ( log ⁡ n ) \mathrm{O}(\log n) O(logn)
      基本同无修改版本,只不过在二分求解前进行一次标记下传。
  • 参考代码

#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
//---//
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;

typedef unsigned int u;
typedef long long ll;
typedef unsigned long long llu;

#define rep(i, a, b) for (ll i = a; i < b; i++)
#define REP(i, a, b) for (ll i = a; i <= b; i++)
#define per(i, b, a) for (ll i = b; i >= a; i--)

// P3372 【模板】线段树 1

#define get_mid ll m = s + ((t - s) >> 1)

const ll N = 1e5 + 10;

ll a[N], d[4 * N], b[4 * N];
bool lz[4 * N];

void build(ll s, ll t, ll p) {
  if (s == t) {
    d[p] = a[s];
    return;
  }
  get_mid;
  build(s, m, 2 * p);
  build(m + 1, t, 2 * p + 1);
  d[p] = d[2 * p] + d[2 * p + 1];
}

ll push_down(ll s, ll t, ll p) {
  get_mid;
  if (s == t || (!lz[p])) return m;
  b[2 * p] += b[p];
  b[2 * p + 1] += b[p];
  d[2 * p] += b[p] * (m - s + 1);
  d[2 * p + 1] += b[p] * (t - m);
  lz[2 * p] = true;
  lz[2 * p + 1] = true;
  b[p] = 0;
  lz[p] = false;
  return m;
}

void update(ll l, ll r, ll c, ll s, ll t, ll p) {
  if (l <= s && t <= r) {
    d[p] += (t - s + 1) * c;
    b[p] += c;
    lz[p] = true;
    return;
  }
  ll m = push_down(s, t, p);
  if (l <= m) update(l, r, c, s, m, 2 * p);
  if (r > m) update(l, r, c, m + 1, t, 2 * p + 1);
  d[p] = d[2 * p] + d[2 * p + 1];
}

ll query(ll l, ll r, ll s, ll t, ll p) {
  if (l <= s && t <= r) {
    return d[p];
  }
  ll sum = 0, m = push_down(s, t, p);
  if (l <= m) sum += query(l, r, s, m, 2 * p);
  if (r > m) sum += query(l, r, m + 1, t, 2 * p + 1);
  return sum;
}

signed main() {
  ll n, m, t, l, r, k;
  scanf("%lld%lld", &n, &m);
  REP(i, 1, n) scanf("%lld", &a[i]);
  build(1, n, 1);
  while (m--) {
    scanf("%lld%lld%lld", &t, &l, &r);
    if (t == 1) {
      scanf("%lld", &k);
      update(l, r, k, 1, n, 1);
    } else
      printf("%lld\n", query(l, r, 1, n, 1));
  }
}

// https://www.luogu.com.cn/record/60473191
  • P3373 【模板】线段树 2 - 洛谷这道题中有两种区间修改操作:加法和乘法,我们需要两个懒惰标记来分别记录它们。
    标记下传中应该先乘后加:
    如将序列先 + k +k +k,再 × m \times m ×m ,再 + t +t +t,则新的元素和应为:
    s u m ′ = ∑ i = l r m ( a i + k ) + t = ( r − l + 1 ) ( m k + t ) ⏟ a d d + m ⏟ m u l ∑ i = l r a i = l e n ∗ a d d + m u l ∗ s u m \mathtt{sum'}=\sum_{i=l}^rm(a_i+k)+t=(r-l+1)\underbrace{(mk+t)}_\mathtt{add}+\underbrace{m}_\mathtt{mul}\sum_{i=l}^ra_i=\mathtt{len*add+mul*sum} sum=i=lrm(ai+k)+t=(rl+1)add (mk+t)+mul mi=lrai=lenadd+mulsum
    由于 a d d = m k + t \mathtt{add}=mk+t add=mk+t,所以将序列 × m \times m ×m 时应将 a d d \mathtt{add} add 值也 × m \times m ×m
  • 参考代码:
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
//---//
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;

typedef unsigned int u;
typedef long long ll;
typedef unsigned long long llu;

#define rep(i, a, b) for (ll i = a; i < b; i++)
#define REP(i, a, b) for (ll i = a; i <= b; i++)
#define per(i, b, a) for (ll i = b; i >= a; i--)

// P3372 【模板】线段树 2

#define get_mid ll m = s + ((t - s) >> 1)

const ll N = 1e5 + 10;

ll mod, a[N], d[4 * N], add[4 * N], mul[4 * N];
bool lz[4 * N];

void build(ll s, ll t, ll p) {
  mul[p] = 1;
  if (s == t) {
    d[p] = a[s];
    return;
  }
  get_mid;
  build(s, m, 2 * p);
  build(m + 1, t, 2 * p + 1);
  d[p] = (d[2 * p] + d[2 * p + 1]) % mod;
}

ll push_down(ll s, ll t, ll p) {
  get_mid;
  if (s == t || (!lz[p])) return m;
  d[2 * p] = (add[p] * (m - s + 1) % mod + d[2 * p] * mul[p] % mod) % mod;
  d[2 * p + 1] = (add[p] * (t - m) % mod + d[2 * p + 1] * mul[p] % mod) % mod;
  add[2 * p] = (add[2 * p] * mul[p] % mod + add[p]) % mod;
  add[2 * p + 1] = (add[2 * p + 1] * mul[p] % mod + add[p]) % mod;
  mul[2 * p] = mul[2 * p] * mul[p] % mod;
  mul[2 * p + 1] = mul[2 * p + 1] * mul[p] % mod;
  lz[2 * p] = true;
  lz[2 * p + 1] = true;
  add[p] = 0;
  mul[p] = 1;
  lz[p] = false;
  return m;
}

void update_add(ll l, ll r, ll c, ll s, ll t, ll p) {
  if (l <= s && t <= r) {
    d[p] = ((t - s + 1) * c % mod + d[p]) % mod;
    add[p] = (c + add[p]) % mod;
    lz[p] = true;
    return;
  }
  ll m = push_down(s, t, p);
  if (l <= m) update_add(l, r, c, s, m, 2 * p);
  if (r > m) update_add(l, r, c, m + 1, t, 2 * p + 1);
  d[p] = (d[2 * p] + d[2 * p + 1]) % mod;
}

void update_mul(ll l, ll r, ll c, ll s, ll t, ll p) {
  if (l <= s && t <= r) {
    d[p] = d[p] * c % mod;
    add[p] = add[p] * c % mod;
    mul[p] = (mul[p] * c) % mod;
    lz[p] = true;
    return;
  }
  ll m = push_down(s, t, p);
  if (l <= m) update_mul(l, r, c, s, m, 2 * p);
  if (r > m) update_mul(l, r, c, m + 1, t, 2 * p + 1);
  d[p] = (d[2 * p] + d[2 * p + 1]) % mod;
}

ll query(ll l, ll r, ll s, ll t, ll p) {
  if (l <= s && t <= r) return d[p];
  ll sum = 0, m = push_down(s, t, p);
  if (l <= m) sum = (sum + query(l, r, s, m, 2 * p)) % mod;
  if (r > m) sum = (sum + query(l, r, m + 1, t, 2 * p + 1)) % mod;
  return sum;
}

signed main() {
  ll n, m, t, l, r, k;
  scanf("%lld%lld%lld", &n, &m, &mod);
  REP(i, 1, n) scanf("%lld", &a[i]);
  build(1, n, 1);
  while (m--) {
    scanf("%lld%lld%lld", &t, &l, &r);
    switch (t) {
      case 1:
        scanf("%lld", &k);
        update_mul(l, r, k, 1, n, 1);
        break;
      case 2:
        scanf("%lld", &k);
        update_add(l, r, k, 1, n, 1);
        break;
      case 3:
        printf("%lld\n", query(l, r, 1, n, 1));
        break;
    }
  }
}

// https://www.luogu.com.cn/record/60476253
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chesium

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值