【算法】线段树

1. 线段树简介#

1.1 前言#

线段树是一个很重要的数据结构,线段树主要用于优化时间复杂度或处理一些较灵活的问题。本文会注重介绍线段树的基础用法。

1.2 什么是线段树?#

线段树是一种特殊的二叉树,其特殊在于每个节点都管辖着一个区间。线段树可以将一段区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

下图就是一个典型的线段树:

(请记住这张图,讲线段树的基本操作的时候会用到!)

我们可以利用线段树进行一些区间操作。

住:以下内容均以区间和为例

1.3 建树#

学过二叉树的同学都知道,有一种二叉树的存储方法就是【顺序存储】。

如下图

1 号节点有两个儿子,编号为 23
2 号节点也有两个儿子,编号为 45
3 号节点只有一个儿子,编号为 6

根据分类,我们可以把一个节点的儿子分为左儿子右儿子,设父节点的编号为 x,则左儿子的节点编号为 2x,右儿子的节点编号为 2x+1。线段树亦是如此。

在 1.2 中提到了"线段树可以将一段区间划分成一些单元区间",观察发现,若父节点所管辖的区间为 [l,r],则左儿子所管辖的区间为 [l,mid],右儿子所管辖的区间为 [mid+1,r]

前置内容都讲完了,正式切入正题:如何建树?

用结构体存储节点的信息,包括节点管辖的区间 [l,r],节点信息,lazy 标记(用于区间修改,留个悬念)。
考虑递归建树,每次将区间分为两部分(即 [l,mid][mid+1,r])。分配给左右儿子,再分别进入左右儿子。若递归到叶节点(即节点所管辖的区间 l=r),直接将叶节点的信息改为想要维护的信息,重复上述过程直至结束。

回溯时记得要把信息合并。

具体实现见代码:

struct Segment_Tree {
  int l, r;
  int num;
  int tag;
}t[N << 2];

void pushup(int p) {
  t[p].num = t[p << 1].num + t[p << 1 | 1].num;
}

void build(int p, int l, int r) {
  t[p].l = l, t[p].r = r;
  if(l == r) {
    t[p].num = a[l]; return ;
  }
  int mid = l + r >> 1;
  build(p << 1, l, mid);
  build(p << 1 | 1, mid + 1, r);
  pushup(p);
}

pushup 函数是将子节点的信息上传至父节点的过程(又称信息合并)。

建树时,build(1, 1, n) 就行了。

1.4 区间查询#

运用了二分 + 分块的思想。
假设要查 [3,7],则可把其分为 [3,4][5,6][7,7] 来求解。
如下图:

ps:黄颜色的点即是被查询的点

这样,我们便可以用 O(log2n) 的时间复杂度来解决区间查询问题。

具体实现见代码:

int query(int p, int l, int r) {
  if(l <= t[p].l && t[p].r <= r) {
    return t[p].num;
  }
  int ans = 0, mid = t[p].l + t[p].r >> 1;
  if(l <= mid) {
    ans += query(p << 1, l, r);
  }
  if(r > mid) {
    ans += query(p << 1 | 1, l, r);
  }
  return ans;
}

单点查询其实就是区间查询的子集,当查询单点 l 时,查询区间 [l,l] 即可。

1.5 区间修改#

注意!难点来了!(敲黑板

类比区间查询,我们发现区间修改需要修改叶子结点的值,也就是每次都需要跑到树的底部修改值,这样时间复杂度会退化到 O(nlog2n)(因为当修改区间 [1,n] 时,需要到 n 个叶子节点去修改值,每一次修改要花费 O(log2n) 的时间复杂度,总时间复杂度 O(nlog2n))。

那这样线段树岂不是没优势了?其实不然,只是方法没用对!

让我们集中注意力,再次类比区间查询。不难发现区间查询并非全部都停留在叶节点做查询。而是在非叶子节点。那区间修改是否能做到在非叶子节点修改呢?

为了解决此问题,这里我们就要引用 lazy 标记(也叫延迟标记) 这个东西。

每次进行区间修改时,在被修改区间所包含的节点上打一个标记,标记的内容就是"这个区间所修改的内容",等此节点的儿子节点被查询时,再将标记下传至子节点,计算答案。

比如:将 [1,8] 这段区间的值加 2,再查询区间 [1,4] 便有以下过程:

1.标记"区间+2"。

2.查询时,标记下传,计算答案。

具体实现见代码:

int len(int p) {
  return t[p].r - t[p].l + 1;
}

void brush(int p, int k) {
  t[p].tag += k;
  t[p].num += len(p) * k;
}

void pushdown(int p) {
  brush(p << 1, t[p].tag);
  brush(p << 1 | 1, t[p].tag);
  t[p].tag = 0;
}

void add(int p, int l, int r, int k) {
  if(l <= t[p].l && t[p].r <= r) {
    brush(p, k);
    return ;
  }
  pushdown(p);
  int mid = t[p].l + t[p].r >> 1;
  if(l <= mid) {
    add(p << 1, l, r, k);
  }
  if(r > mid) {
    add(p << 1 | 1, l, r, k);
  }
  pushup(p);
}

pushdown 函数是将标记下传的过程。
brush(奇怪的名字)函数是将节点打上标记的过程。
len 函数计算节点所管辖的区间的长度。

单点修改其实就是区间修改的子集,当单点修改 l 时,修改区间 [l,l] 即可。

2.线段树杂项#

2.1 线段树时间复杂度分析#

操作 最优时间复杂度 最劣时间复杂度 常数
建树 O(n) O(n)
单点修改 O(log2n) O(log2n)
单点查询 O(log2n) O(log2n) 较小
区间修改 O(log2n) O(log2n)
区间查询 O(log2n) O(log2n) 较大

总结:线段树固然好用,但常数异常的大,请谨慎使用!

2.2 线段树的空间#

一般来讲,线段树的空间大小是数组长度的 4 倍。(一般是这样,但也有特例)

万一数组长度达到了 5e6 甚至更长,而此时又需要使用线段树时,最好的方法是:动态开点

动态开点的精髓就在于其没有用过的点是不会占用线段树的空间的(与 vector 有异曲同工之妙),但代价就是写起来麻烦,出错了又很难调。所以不到迫不得已,不要轻易使用动态开点。

2.3 线段树的注意事项#

  1. 很多人写线段树都没有存每一个节点所管辖的区间的习惯,而是在每一次操作时重新计算。这样既浪费时间又容易出错。建议在建树时就预处理出每一个节点所管辖的区间,并存起来。操作的时候便可以快速使用;

  2. 在使用线段树前记得要建树(应该不会有人忘记,毕竟建树是一个很重要也很关键的操作);

  3. 有区间修改时,每一次操作(除了建树)都需要 pushdown,这样才能保证所求的区间的值是正确的;

  4. pushdown 的顺序也很重要,例如:当修改、乘法和加法标记同时存在时,下传的顺序应为:先传修改,再传乘法,最后传加法。(加法和乘法之间也有一定的联系,可以参考这道题

  5. 线段树的空间一定要开 4 倍!线段树的空间一定要开 4 倍!线段树的空间一定要开 4 倍! (重要的事情说三遍)!

2.4 线段树的blogs#

如果您觉得本篇 blogs 晦涩难懂,以下的几篇 blogs 是您最好的选择!!

以下是我的朋友们写的:

  1. 【算法】线段树 from Arcka

  2. 线段树学习笔记(入门) from sheeplittlecloud

(爆推!!!两位博主的其他算法总结也写得非常好!)

以下是我觉得写得不错的

  1. 浅谈线段树(Segment Tree)

3. 线段树例题#

3.1 [luogu]P3372 【模板】线段树 1#

Problem#

已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 k
  2. 求出某区间每一个数的和。

Solve#

标准的线段树模板题

线段树的区间修改和区间查询

Code#

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <climits>
#include <map>
#include <queue>
#include <set>
#include <cmath>
#include <string>
#define int long long
#define rint register int
#define For(i,l,r) for(int i=l;i<=r;i++)
#define FOR(i,r,l) for(int i=r;i>=l;i--)
#define mod 1000000007

using namespace std;

inline int read() {
  rint x=0,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();}
  return x*f;
}

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 = 100010;

struct Node {
  int l, r;
  int num;
  int tag;
}t[N << 2];

int n = read(), m = read(), a[N], x, y;

int len(int p) {
  return t[p].r - t[p].l + 1;
}

void brush(int p, int k) {
  t[p].tag += k;
  t[p].num += len(p) * k;
}

void pushdown(int p) {
  brush(p << 1, t[p].tag);
  brush(p << 1 | 1, t[p].tag);
  t[p].tag = 0;
}

void pushup(int p) {
  t[p].num = t[p << 1].num + t[p << 1 | 1].num;
}

void build(int p, int l, int r) {
  t[p].l = l, t[p].r = r;
  if(l == r) {
    t[p].num = a[l]; return ;
  }
  int mid = l + r >> 1;
  build(p << 1, l, mid);
  build(p << 1 | 1, mid + 1, r);
  pushup(p);
}

void add(int p, int l, int r, int k) {
  if(l <= t[p].l && t[p].r <= r) {
    brush(p, k);
    return ;
  }
  pushdown(p);
  int mid = t[p].l + t[p].r >> 1;
  if(l <= mid) {
    add(p << 1, l, r, k);
  }
  if(r > mid) {
    add(p << 1 | 1, l, r, k);
  }
  pushup(p);
}

int query(int p, int l, int r) {
  if(l <= t[p].l && t[p].r <= r) {
    return t[p].num;
  }
  pushdown(p);
  int ans = 0, mid = t[p].l + t[p].r >> 1;
  if(l <= mid) {
    ans += query(p << 1, l, r);
  }
  if(r > mid) {
    ans += query(p << 1 | 1, l, r);
  }
  return ans;
}

signed main() {
  For(i,1,n) a[i] = read();
  build(1, 1, n);
  For(i,1,m) {
    int op, k;
    op = read(), x = read(), y = read();
    if(op == 1) {
      k = read();
      add(1, x, y, k);
    } else {
      print(query(1, x, y));puts("");
    }
  }
  return 0;
}

作者:Daniel-yao

出处:https://www.cnblogs.com/Daniel-yao/p/17048128.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Daniel_yzy  阅读(211)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示