线段树的基本知识和初级运用

前言

线段树绝对是出题人最爱考的高级数据结构了。它快、灵活、码量也大,相当考验 OIer 的综合能力。所以好好学习一下线段树是相当必要的。

基础

线段树是基于二叉树的。通过为二叉树的每个节点赋予线段的意义,线段树可以维护很多的区间信息,包括但不限于区间和、区间最大值、区间第 k 大(这玩意是可持久化线段树维护的)。另外,由于它是基于二叉树的,所以它可以在 \(\Theta(\log n)\) 的时间复杂度内实现区间修改和区间查询。
这里借用 OI wiki 的一张图来帮助理解。(其中d[i] 表示线段树的节点,“\(=\)”后面维护的是区间和,红色方括号括起来的是节点表示的线段):
以下以区间修改求区间和为例,给出线段树的示例代码。

洛谷 P3372 【模板】线段树 1

给出一个长度为 \(n\) 的序列 \(a\),并进行以下操作共 \(m\) 次:

  1. 将区间 \([l..r]\) 的每一个数加上 \(k\)
  2. 求区间 \([l..r]\) 的和。

更新答案

通过惊人的注意力,你应该可以发现:在上图中,每一个节点 \(u\) 的左儿子编号是 \(u \times 2\),右儿子编号是 \(u\times 2 + 1\),所以找左右儿子就可以简化为:

//使用位运算可以达到同样的效果,且常数更优
#define ls(u) (u << 1)
#define rs(u) (u << 1 | 1)

那么更新答案的函数就是:

// u 表示正在更新的节点编号
void pushup(int u) { sum[u] = sum[ls(u)] + sum[rs(u)]; }

建树

递归即可:

void build(int u, int l, int r) { //在节点 u 对区间 [l, r] 建树if (l == r) return sum[u] = a[l], void(); //return 的值是最后一个,这里就是 void() 占位符,其实就是没有返回值的意思int mid = (l + r) >> 1; //防溢出可以 mid = l + ((r - l) >> 1)build(ls(u), l, mid), build(rs(u), mid + 1, r);pushup(u);
}

注意 sum 的空间应当开为 a\(4\) 倍(准确来说是 \(2^{\left\lceil \log n \right\rceil + 1}\) 倍),即 int a[N], sum[N << 2]

区间查询

假如我们现在要查询 \([3..5]\) 的区间和,可以用 \([3..3]\) 的区间和加上 \([4..5]\) 的区间和。
一般地,如果查询区间 \([l..r]\),则可以将其分割成 \(O(\log n)\) 个极大区间,使得这些区间的答案合并得到区间 \([l..r]\) 的答案。
代码如下:

//当前节点为 u,u 所代表的区间为 [l..r],要查询的区间为 [ql..qr]
int query(int u, int l, int r, int ql, int qr) {if (l > qr || r < ql) return 0; //完全不是要找的区间,退回if (l >= ql && r <= qr) return sum[u]; //当前区间是询问区间的一个极大子集int mid = l + ((r - l) >> 1), res = 0;if (ql <= mid) res += query(ls(u), l, mid, ql, qr);if (qr > mid) res += query(rs(u), mid + 1, r, ql, qr);return res;
}

区间修改

如果每一次都遍历所有 \([l..r]\) 的极大子区间来修改,时间复杂度难以接受。
还记得堆的删除中用的懒标记吗?我们考虑往线段树里也引入懒标记。具体地,我们给每一个节点新开一个 tag 数组来记录节点对应的修改,而只有当访问到该节点时,它所持有的懒标记才生效。每次生效一个标记的同时,往这个节点的儿子下传标记。
本题的增加操作是可累加的,所以多个标记的合并用累加即可。(如果是区间改为 \(k\),那么标记的合并就应该用覆盖;具体情况具体分析。)
打标记代码:

//为表示 [l..r] 的节点 u 打上加 d 的标记
void addtag(int u, int l, int r, int d) {tag[u] += d;sum[u] += d * (r - l + 1);//当然也可以不在这里修改,而是到叶节点修改后靠 pushup 更新上面的答案
}

下传标记代码:

void pushdown(int u, int l, int r) {int mid = (l + r) >> 1;addtag(ls(u), l, mid);addtag(rs(u), mid + 1, r);tag[u] = 0;
}

区间修改代码:

void update(int u, int l, int r, int ql, int qr, int x) {if (ql <= l && qr >= r) return addtag(u, l, r, x), void();pushdown(u, l, r);int mid = (l + r) >> 1;if (ql <= mid) update(ls(u), l, mid, ql, qr, x);if (qr > mid) update(rs(u), mid + 1, r, ql, qr, x);pushup(u);
}

并且因为有了 pushdown 这个操作,区间询问的代码也得有点微调:

int query(int u, int l, int r, int ql, int qr) {if (l >= ql && qr >= r) return sum[u];pushdown(u, l, r);int mid = (l + r) >> 1, res = 0;if (ql <= mid)res += query(ls(u), l, mid, ql, qr);if (qr > mid)res += query(rs(u), mid + 1, r, ql, qr);return res;
}

于是这道题的代码就写完了:

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100010;
int n, m;
int a[N], tree[N << 2], tag[N << 2];
#define ls(u) (u << 1)
#define rs(u) (u << 1 | 1)
void pushup(int p) {tree[p] = tree[ls(p)] + tree[rs(p)];
}
void build(int p, int pl, int pr) {tag[p] = 0;if (pl == pr) return tree[p] = a[pl], void();int mid = (pl + pr) >> 1;build(ls(p), pl, mid), build(rs(p), mid + 1, pr);pushup(p);
}
void addtag(int p, int pl, int pr, int d) {tag[p] += d;tree[p] += (pr - pl + 1) * d;
}
void pushdown(int p, int pl, int pr) {if (tag[p]) {int mid = (pl + pr) >> 1;addtag(ls(p), pl, mid, tag[p]), addtag(rs(p), mid + 1, pr, tag[p]);tag[p] = 0;}
}
void update(int L, int R, int p, int pl, int pr, int d) {if (L <= pl && R >= pr)return addtag(p, pl, pr, d), void();pushdown(p, pl, pr);int mid = (pl + pr) >> 1;if (L <= mid)update(L, R, ls(p), pl, mid, d);if (R > mid)update(L, R, rs(p), mid + 1, pr, d);pushup(p);
}
int query(int L, int R, int p, int pl, int pr) {if (L <= pl && R >= pr)return tree[p];pushdown(p, pl, pr);int res = 0;int mid = (pl + pr) >> 1;if (L <= mid)res += query(L, R, ls(p), pl, mid);if (R > mid)res += query(L, R, rs(p), mid + 1, pr);return res;
}
signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);cin >> n >> m;for (int i = 1; i <= n; i++) cin >> a[i];build(1, 1, n);while (m--) {int opt, x, y, z;cin >> opt;if (opt == 1) cin >> x >> y >> z, update(x, y, 1, 1, n, z);else cin >> x >> y, cout << query(x, y, 1, 1, n) << "\n";}return 0;
}

例题

下面再来看几道例题吧:

P3373 【模板】线段树 2

题意简述:给出一个序列,支持区间加、区间乘、区间求和;\(n, q\le 10^5\)
题意解析:就是 P3372 【模板】线段树 1 多一个区间乘操作,额外开一个懒标记表示乘即可。
注意到加和乘不是同级运算。使用乘法分配律 \(a(b + c) = ab + ac\) 来同时操作两个 tag 即可。
AC 代码:

#include <bits/stdc++.h>
using namespace std;
#define int long longinline int rd() {int a = 0, sgn = 1;char c = getchar();for (; !isdigit(c); c = getchar()) if (c == '-') sgn = -1;for (; isdigit(c); c = getchar()) a = (a << 3) + (a << 1) + c - '0';return a * sgn;
}
inline void wt(int a) {if (a < 0) putchar('-'), a = -a;int sta[35], top = 0;do { sta[top++] = a % 10; } while (a /= 10);while (top--) putchar(sta[top] + '0');
}const int N = 1e5 + 3;
int a[N], n, m, Mod;struct SegmentTree {int tree[N << 2];int tag1[N << 2], tag2[N << 2];int ls(int k) { return k << 1; }int rs(int k) { return k << 1 | 1; }void pushup(int u) { tree[u] = ((tree[ls(u)] %= Mod) + (tree[rs(u)] %= Mod)) % Mod; }void build(int u, int l, int r) {tag1[u] = 0, tag2[u] = 1;if (l == r) { return tree[u] = a[l], void(); }int mid = (l + r) >> 1;build(ls(u), l, mid), build(rs(u), mid + 1, r);pushup(u);}void addtag1(int u, int l, int r, int d) {(tag1[u] += (d % Mod))                       %= Mod;(tree[u] += (d % Mod) * ((r - l + 1) % Mod)) %= Mod;}void addtag2(int u, int l, int r, int d) {(tag1[u] *= d % Mod) %= Mod;(tag2[u] *= d % Mod) %= Mod;(tree[u] *= d % Mod) %= Mod;}void pushdown(int u, int l, int r) {int mid = (l + r) >> 1;addtag2(ls(u), l, mid, tag2[u]);addtag1(ls(u), l, mid, tag1[u]);addtag2(rs(u), mid + 1, r, tag2[u]);addtag1(rs(u), mid + 1, r, tag1[u]);tag1[u] = 0, tag2[u] = 1;}void update1(int u, int l, int r, int ql, int qr, int d) {if (ql <= l && qr >= r)return addtag1(u, l, r, d), void();pushdown(u, l, r);int mid = (l + r) >> 1;if (ql <= mid) update1(ls(u), l, mid, ql, qr, d);if (qr > mid)  update1(rs(u), mid + 1, r, ql, qr, d);pushup(u);}void update2(int u, int l, int r, int ql, int qr, int d) {if (ql <= l && qr >= r)return addtag2(u, l, r, d), void();pushdown(u, l, r);int mid = (l + r) >> 1;if (ql <= mid) update2(ls(u), l, mid, ql, qr, d);if (qr > mid)  update2(rs(u), mid + 1, r, ql, qr, d);pushup(u);}int query(int u, int l, int r, int ql, int qr) {if (l >= ql && qr >= r) return tree[u];pushdown(u, l, r);int mid = (l + r) >> 1;int res = 0;if (ql <= mid) res += query(ls(u), l, mid, ql, qr);if (qr > mid)  res += query(rs(u), mid + 1, r, ql, qr);return res;}
} st;main() {n = rd(), m = rd(), Mod = rd();for (int i = 1; i <= n; i++)a[i] = rd();st.build(1, 1, n);for (int type; m--; ) {type = rd();if (type == 3) {int l = rd(), r = rd();wt(st.query(1, 1, n, l, r) % Mod), puts("");}if (type == 2) {int l = rd(), r = rd(), c = rd();st.update1(1, 1, n, l, r, c);}if (type == 1) {int l = rd(), r = rd(), c = rd();st.update2(1, 1, n, l, r, c);}}
}

P3178 [HAOI2015] 树上操作

题意简述:给一颗树,以 \(1\) 为根,树有点权。支持单点点权增加,点到根路径点权增加,点到根路径点权求和;\(n, m\le 10^5\)
题意解析:求出这棵树的欧拉序(不懂欧拉序的看这篇博客或者期待一下以后本蒟蒻写一篇)。利用欧拉序的性质将点到根的路径转化为欧拉序的前缀。对区间加 \(k\) 的实际影响是 \(k\times(cnt_+ - cnt_-)\)\(cnt_+\) 表示区间内进入节点的次数(即 \(+x\) 的个数),\(cnt_-\) 表示区间内离开节点的次数(即 \(-x\) 的个数)。
AC 代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
inline int rd() {int a = 0, sgn = 1;char c = getchar();for (; !isdigit(c); c = getchar()) if (c == '-') sgn = -1;for (; isdigit(c); c = getchar()) a = (a << 3) + (a << 1) + c - '0';return a * sgn;
}
inline void wt(int a) {if (a < 0) putchar('-'), a = -a;int sta[35], top = 0;do { sta[top++] = a % 10; } while (a /= 10);while (top--) putchar(sta[top] + '0');
}
const int N = 100003;
#define ls(x) (x) << 1
#define rs(x) (x) << 1 | 1
int n, m, a[N], tid[N], tif[N], num[N << 1], stamp;
int ecnt, head[N], vet[N << 1], nxt[N << 1];
int val[N << 3], tag[N << 3];
struct DFN {int f, v;
} dfspath[N << 1];
void add(int u, int v) {vet[++ecnt] = v;nxt[ecnt] = head[u];head[u] = ecnt;
}
void dfs(int u, int f) {dfspath[tid[u] = ++stamp].f = 1;dfspath[stamp].v = u;for (int e = head[u]; e; e = nxt[e])if (vet[e] != f)dfs(vet[e], u);dfspath[tif[u] = ++stamp].f = -1;dfspath[stamp].v = u;
}
void pushdown(int u, int l, int m, int r) {if (tag[u]) {val[ls(u)] += tag[u] * (num[m] - num[l - 1]);val[rs(u)] += tag[u] * (num[r] - num[m]);tag[ls(u)] += tag[u];tag[rs(u)] += tag[u];tag[u] = 0;}
}
void build(int u, int l, int r) {if (l == r)return val[u] = dfspath[l].f * a[dfspath[l].v], void();int m = (l + r) >> 1;build(ls(u), l, m), build(rs(u), m + 1, r);val[u] = val[ls(u)] + val[rs(u)];
}
void update(int u, int l, int r, int x, int c) {val[u] += c;if (l == r)return;int m = (l + r) >> 1;if (x <= m)update(ls(u), l, m, x, c);elseupdate(rs(u), m + 1, r, x, c);
}
void change(int u, int l, int r, int p, int q, int c) {if (p <= l && r <= q) {tag[u] += c;val[u] += (int)c * (num[r] - num[l - 1]);return;}int m = (l + r) >> 1;pushdown(u, l, m, r);if (p <= m)change(ls(u), l, m, p, q, c);if (q > m)change(rs(u), m + 1, r, p, q, c);val[u] = val[ls(u)] + val[rs(u)];
}
int query(int u, int l, int r, int p, int q) {if (p <= l && r <= q)return val[u];int m = (l + r) >> 1;int res = 0;pushdown(u, l, m, r);if (p <= m)res += query(ls(u), l, m, p, q);if (q > m)res += query(rs(u), m + 1, r, p, q);return res;
}
main() {n = rd(), m = rd();for (int i = 1; i <= n; ++i) a[i] = rd();for (int i = 1, u, v; i < n; ++i) u = rd(), v = rd(), add(u, v), add(v, u);dfs(1, 0);for (int i = 1; i <= stamp; ++i) num[i] = num[i - 1] + dfspath[i].f;build(1, 1, n + n);for (int opt, x, a; m--;) {opt = rd();if (opt == 1)x = rd(), a = rd(), update(1, 1, n + n, tid[x], +a), update(1, 1, n + n, tif[x], -a);else if (opt == 2)x = rd(), a = rd(), change(1, 1, n + n, tid[x], tif[x], a);elsex = rd(), wt(query(1, 1, n + n, 1, tid[x])), puts("");}
}

结语

线段树还有很多进阶的知识点。碍于篇幅,本篇仅介绍了线段树的基本原理和初级运用。想看更多有关线段树的内容可以期待一下以后写的博客,这里就先咕咕掉了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/736786.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信息摘要技术

信息摘要 定义 信息摘要就是原数据通过某个算法生成一个固定长度的单向散列值。 特点 1、输出长度固定:无论输入的长度和值如何,计算得到的哈希值长度总是固定的。 2、不可逆性(单向):不可能使用散列值推出原文件内容 3、无碰撞性:想要找到两个不同的输入值,使它们哈希后…

完美解决stack Error: Can‘t find Python executable “python“, you can set the PYTHON env variable.

解决方案:node版本太高了,我同时说他环境是node14的,我就来了个14.18的,结果还是不是,应该是14系列,我的二级版本还是高了。 python什么的安装了没什么用!!! 一步一步来,先解决第一部分: 错误提示的意思是说我没有python,我电脑里确实没有下载python,但实际上不用下…

DVT:华为提出动态级联Vision Transformer,性能杠杠的 | NeurIPS 2021

论文主要处理Vision Transformer中的性能问题,采用推理速度不同的级联模型进行速度优化,搭配层级间的特征复用和自注意力关系复用来提升准确率。从实验结果来看,性能提升不错 来源:晓飞的算法工程笔记 公众号论文: Not All Images are Worth 16x16 Words: Dynamic Transfor…

论文查重

代码说明 类的设计 PaperPlagiarismChecker 类 这个类是算法的核心,负责实现论文查重的具体逻辑和计算。它可能包含以下主要函数: calculateCosineSimilarity(String text1, String text2): 计算两个文本之间的余弦相似度。这是核心的相似度计算函数。 getWordFrequency(Stri…

常见排序原理及 python 实现

时间复杂度与空间复杂度 常用O(1)或O(n)表示,其中1表示一个单位(最简单的单位,可以是多个或1个,但在时间上总体是较低且连续的),时间通常指的是程序运行时间,空间则是指程序在运行时所占用的内存空间。各个阶段的复杂度可用下面的顺序比较: O(1) < O(logn) < O(n) &…

【日记】在街上跳舞被同事看见了(470 字)

正文昨晚跳舞,照例在街上表演,被单位里的保洁阿姨撞见了…… 我以为这就完了,结果她还拍了视频发给做饭阿姨。晚上吃饭无意间聊起才知道有这回事。我竟一时间不知该哭还是该笑…….今天非常非常闲。虽然不是没工作,只是我懒得去做,堆在那里罢了。下午还差点跟主管吵起来(…

使用Filter接口编写过滤器解决post乱码

在使用tomcat9以及之前的版本,request-character-encoding和response-character-encoding使用的字符编码默认不是utf-8,所以导致前端发送到后台的中文乱码.如果使用的是tomcat10以及之后的版本,在apache-tomcat-10.1.25\conf\web.xml已设置好默认的字符集编码为utf-8,如果所示:…

使用EF 连接 数据库 SQLserver、MySql 实现 CodeFirst

1.新建项目,下载Nuget安装包 创建项目需要注意几点,如果是基于 .net framework 的项目 需要选择 相应版本的 EF, 如果是跨平台则选择EF Core版本。 我这里选择的是 .net framework 版本。红框里面是 实现EF Code First 需要的包。对应的版本: EntityFramework 6.3.0 MySql.D…

AGNN论文阅读笔记

Attention-Based Graph Neural Network for News Recommendation论文阅读笔记 Abstract 存在的问题: ​ 用户的历史点击序列信息对用户兴趣的影响也不尽相同,简单地将它们结合起来并不能反映这种差异。 提出方法: ​ 我们提出了一种基于注意力的图神经网络新闻推荐模型。在我…