Loading

【算法】线段树

1. 线段树简介

1.1 前言

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

1.2 什么是线段树?

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

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

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

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

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

1.3 建树

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

如下图

\(1\) 号节点有两个儿子,编号为 \(2\)\(3\)
\(2\) 号节点也有两个儿子,编号为 \(4\)\(5\)
\(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(\log_2n)\) 的时间复杂度来解决区间查询问题。

具体实现见代码:

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(n\log_2n)\)(因为当修改区间 \([1,n]\) 时,需要到 \(n\) 个叶子节点去修改值,每一次修改要花费 \(O(\log_2n)\) 的时间复杂度,总时间复杂度 \(O(n\log_2n)\))。

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

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

为了解决此问题,这里我们就要引用 \(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(\log_2 n)\) \(O(\log_2 n)\)
单点查询 \(O(\log_2 n)\) \(O(\log_2 n)\) 较小
区间修改 \(O(\log_2 n)\) \(O(\log_2 n)\)
区间查询 \(O(\log_2 n)\) \(O(\log_2 n)\) 较大

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

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;
}

posted @ 2023-01-13 15:33  Daniel_yzy  阅读(181)  评论(3编辑  收藏  举报