EVERYTHING HAPPENS FOR THE BEST!|

wnsyou

园龄:2年4个月粉丝:19关注:16

2023-08-18 23:07阅读: 53评论: 0推荐: 1

线段树与树状数组

线段树

OI-wiki Link

线段树是一种支持修改、用于维护区间信息的数据结构,可以在 O(logn) 的复杂度下求出一个大小为 n 的数组的区间信息(如区间和、区间最大值等),也可以在同样时间复杂度下实现单点修改和区间修改等操作

静态区间和可以使用前缀和优化,但如果有修改操作呢?当你更新一个点 i 时,前缀和数组中 i,i+1,i+2n 都要更新,时间复杂度来到了 O(n),无法接受,这时候我们就需要使用线段树。

基本结构

假设现在有一个大小为 5 的数组 a={10,11,12,13,14},用线段树维护区间和如下:

(上图来源 OI-wiki)

建树(自制)

大致思想:最初有一个区间 [1,n],对于一个出现在线段树的区间 [l,r](lr),将其分为左右两个区间 [l,l+r2][l+r2+1,r],各自处理,然后再结合左右区间更新区间信息

区间肯定不能 O(n2) 记录,为了防止区间编号冲突,可以把编号为 i 的节点的左儿子编号设为 2×i,右儿子编号设为 2×i+1

空间分析

现在有一个问题:编号最大为多少?

通过观察,容易发现线段树深度为 logn,则编号最大为 2logn+11,稍加计算可得编号不超过 4×n

详细证明请右转 OI-wiki

Code

int n, a[N], tr[4 * N];
// Make Tree 建树
void MT (int id, int l, int r) { // 当前节点编号为 id,区间范围 [l, r]
if (l == r) {
tr[id] = a[l];
return ;
}
int mid = (l + r) >> 1;
MT(id * 2, l, mid), MT(id * 2 + 1, mid + 1, r);
tr[id] = tr[id * 2] + tr[id * 2 + 1];
}

复杂度 O(n)

区间查询

image

对于上面的例子,我们现在需要查询某些区间的和。

如果是查询 [1,5],很明显,d1 即可,可要是要求 [2,5] 的怎么办呢?

既然 [2,5] 并没有直接出现,那么考虑将其分为若干个出现在线段树上的区间进行求解

大致做法

  • 如果当前遍历到的区间 id[l,r] 被查询区间完全包含,那么可以直接计算当前区间对答案的贡献,即 did
  • 如果当前遍历到的区间 id[l,r] 与查询区间无交集,直接 return ;
  • 否则,将其分为左右两个区间进行查询,即查询 id×2[l,l+r2]id×2+1[l+r2+1,r]

时间复杂度分析

做法了解了,接下来就是分析时间复杂度了。

我们可以把 [l,r] 分成 [l,l],[l+1,l+1],[l+2,l+2],[r1,r1],[r,r]尽量合并

由于查询是一段连续区间,所以当你把所有可以合并的区间都合并之后,线段树每层最多只会有两个区间,时间复杂度为 O(logn)

Code

int qry;
// Query 查询
void Query (int id, int l, int r, int x, int y) { // 查询区间 [x, y]
if (l >= x && r <= y) { // 当前区间被查询区间完全包含
qry += tr[id];
return ;
}
if (l > y || r < x) { // 当前区间与查询区间无交集
return ;
}
int mid = (l + r) >> 1;
Query(id * 2, l, mid, x, y), Query(id * 2 + 1, mid + 1, r, x, y);
}

单点修改

image

继续使用上面的例子,如果我们要修改 a213,该如何更新线段树呢?

image

如图,当你找到区间 [2,2] 对应线段树上哪个节点时,你可以直接将其修改,然后再从下往上重新更新线段树每个节点即可,时间复杂度 O(logn)

如何寻找区间对应节点呢?当我们考虑到一个包含修改目标的区间 id[l,r](lr),很明显左右两个区间有且仅有一个区间包含修改目标,继续寻找即可,时间复杂度 O(logn)

总时间复杂度为 O(logn)

Code

// modify 单点修改
void modify (int id, int l, int r, int x, int y) { // 将 a[x] 修改为 y
if (l == r) { // 找到修改目标
tr[id] = y; // 直接修改
return ;
}
int mid = (l + r) >> 1;
if (mid >= x) { // 修改目标在左半区间
modify(id * 2, l, mid, x, y);
} else { // 修改目标在右半区间
modify(id * 2 + 1, mid + 1, r, x, y);
}
tr[id] = tr[id * 2] + tr[id * 2 + 1]; // 重新更新
}

区间修改与懒标记

单点修改解决,然后就是区间修改。如果对于区间内每个元素都进行一次单点修改,时间复杂度无法接受,需要使用懒标记。

懒标记 lazy tag

懒标记,顾名思义就是一种十分懒惰的标记,用于临时记录区间操作对于当前节点对应范围造成的影响,当它要访问它的左右儿子时,需要将懒标记下传并更新左右儿子的节点信息,懒标记初始为 0

引入懒标记,初始状态:

(上图来源 OI-wiki)

懒标记下传 Code

int lzy[4 * N];
// 将 id[l, r] 的懒标记下传
void pushdown (int id, int l, int r) {
int mid = (l + r) >> 1;
tr[id * 2] += lzy[id] * (mid - l + 1); // 左区间 tr 更新
tr[id * 2 + 1] += lzy[id] * (r - mid); // 右区间 tr 更新
lzy[id * 2] += lzy[id]; // 左区间 lazy tag 更新
lzy[id * 2 + 1] += lzy[id]; // 右区间 lazy tag 更新
lzy[id] = 0; // 清空当前节点 lazy tag
}

有了懒标记,我们就可以实现 O(logn) 的区间修改了。

把 a 中 [3, 5] 每个元素加 5(上图来源 OI-wiki)

操作类似区间查询(将区间 [x,y] 每个元素增加 z):

  • 如果当前遍历到的区间 id[l,r] 被修改区间完全包含,则更新当前节点的懒标记(tid+=z)和答案(did+=z×(rl+1))。
  • 如果当前遍历到的区间 id[l,r] 与修改区间无交集,直接 return ;
  • 否则,将其分为左右两个区间各自修改,即修改 id×2[l,l+r2]id×2+1[l+r2+1,r]

区间修改(加) Code

// modify 区间修改-加
void modify (int id, int l, int r, int x, int y, int z) { // 将 a[x ~ y] 每个元素加 z
if (l >= x && r <= y) { // 当前区间被修改区间完全包含
lzy[id] += z, tr[id] += (r - l + 1) * z;
return ;
}
if (l > y || r < x) { // 当前区间与修改区间无交集
return ;
}
int mid = (l + r) >> 1;
pushdown(id, l, r); // 懒标记下传
modify(id * 2, l, mid, x, y, z), modify(id * 2 + 1, mid + 1, r, x, y, z);
tr[id] = tr[id * 2] + tr[id * 2 + 1]; // 重新更新当前节点
}

区间修改(赋值) Code

容易发现,一个数只与最后一次对它的赋值操作有关。所以懒标记下传与更新需要一点点的更改。

// 将 id[l, r] 的懒标记下传
void pushdown (int id, int l, int r) {
int mid = (l + r) >> 1;
tr[id * 2] = lzy[id] * (mid - l + 1); // 左区间 tr 更新
tr[id * 2 + 1] = lzy[id] * (r - mid); // 右区间 tr 更新
lzy[id * 2] = lzy[id]; // 左区间 lazy tag 更新
lzy[id * 2 + 1] = lzy[id]; // 右区间 lazy tag 更新
lzy[id] = 0; // 清空当前节点 lazy tag
}
// modify 区间修改-赋值
void modify (int id, int l, int r, int x, int y, int z) { // 将 a[x ~ y] 每个元素赋值为 z
if (l >= x && r <= y) { // 当前区间被修改区间完全包含
lzy[id] = z, tr[id] = (r - l + 1) * z;
return ;
}
if (l > y || r < x) { // 当前区间与修改区间无交集
return ;
}
int mid = (l + r) >> 1;
pushdown(id, l, r); // 懒标记下传
modify(id * 2, l, mid, x, y, z), modify(id * 2 + 1, mid + 1, r, x, y, z);
tr[id] = tr[id * 2] + tr[id * 2 + 1]; // 重新更新当前节点
}

老师做的视频

推销一下:

优化

  1. 叶子节点没有儿子,所以懒标记不用下传到叶子节点。
  2. 根据儿子更新当前节点的操作可以写一个函数 pushup,增加代码可读性。
  3. 标记永久化:在确定懒标记不会发生溢出的情况下,可以选择不清空懒标记,只计算对答案的贡献(用处不大,主要用于可持久化数据结构)。
  4. 动态开点:左右儿子不设为 id×2id×2+1,而是设为线段树中没有出现的最小 id。线段树第一层节点数最多为 1,第二层最多为 2,第三层最多为 4,依次类推,可以计算出节点数量最多为 2×n1,可以节省空间。

树状数组

OI-wiki Link

树状数组,是一种用于维护单点修改和区间查询的数据结构。

一些要求

普通树状数组要求维护的信息和运算满足结合律可差分(具有逆运算),包括加法、乘法、异或等。

注意:

  • 模意义下乘法若要可差分,需要保证每个元素都存在逆元。
  • 区间极值、最大公因数等无法用普通树状数组维护(但有办法解决,详见 OI-wiki)。

初步感知

假设现在有一个数组 a,如果我们要求 1i7ai,很明显是算 a1a7 这七个数的和,那如果令 A=a1+a2+a3+a4B=a5+a6C=a7,那么你肯定会说答案就是 A+B+C

这就是树状数组,可以在预处理出一些区间的和之后,把一段前缀化为不超过 logn 个预处理过的区间,以 O(logn) 的方式求出前缀和。

(上图来源 OI-wiki)

上图中的 c 数组用于存储数组 a 的某些区间的和,可以发现 ci 的右端点为 i,可左端点呢?

区间管辖范围

树状数组规定:右端点为 i 的区间的大小为 2ki,其中 k 表示 i 在二进制表示下最低位的 1 的位数(最低位位数为 0)。

lowbit(i) 2ki,那么根据位运算知识,我们可以知道 lowbit(x) = x & (-x),这个东西和原、反、补码有关,这里就不详说了,具体可以看 OI-wiki

构建树状数组

强调:必须确保维护的信息是可差分的,例如求区间和,而区间极值则不可以用树状数组进行维护。

现在给定一个大小为 n 的数组 a,要维护区间和,怎么构建树状数组呢?

可以想到一种比较简单的方法:对于每个 1in,求一下 ilowbit(i)+1jiai,那么时间复杂度为多少呢?

通过找规律,可以发现这个的时间复杂度在 n=107 也只是来到了 12×n 左右,甚至达不到 nlogn,也就是说,在正常情况下是完全可以通过的。

验证 Code

#include <iostream>
using namespace std;
int lowbit (int x) {
return x & -x;
}
int n, ans;
int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
ans += lowbit(i);
}
cout << ans;
return 0;
}

Code

void MT () {
for (int i = 1; i <= n; i++) {
for (int j = i - lowbit(i) + 1; j <= i; j++) { // 统计范围内的整数和
tr[i] += a[j];
}
}
}

当然如果你提前用前缀和进行预处理的话,可以直接 O(n) 解决。

区间查询

举个例子,现在要求区间 [3,5] 的和,那么就可以将其分为两个前缀 [1,5][1,2] 分别进行求解,然后做差(前缀和思想)。

根据区间管辖范围,我们可以轻松地推出前缀和的求法:函数 Query(x) 求解的是前缀 [1,x] 的和,先算上以 x 结尾的区间和 cx,求解范围为 [xlowbit(x)+1,x],也就是说还差 [1,xlowbit(x)] 没有求解,也就是 Query(x - lowbit(x)),如果现在 x = 0,即已将前缀完全求解,那么直接返回 0 即可。

Query(x)={0x=0cx+Query(xlowbit(x))x1

Code

inline int lowbit (int x) {
return x & -x; // 位运算求 lowbit
}
int n, tr[N]; // tr 就是 c 数组
int Query (int x) {
return (x ? tr[x] + Query(x - lowbit(x)) : 0); // 分类讨论
}

单点修改

树状数组の一些性质

lxxlowbit(x)+1

  1. 对于任意 xy,要么 ly>x,要么 lylx
  2. 对于任意 x,有 lx+lowbit(x)lx
  3. 对于任意 x<y<x+lowbit(x),有 ly>x

详细证明见 OI-wiki


假设现在要将 ax 加上 y

为了快速更新 c,我们只需要更新所有包含 xc 即可。

根据如上几个性质,可以推出更新方法:当 ca 包含 x 时,更新 ca,并更新 ca+lowbit(a),以此类推,直到 a>n,此时也就是更新完毕了,很明显 cx 是一定包含 x 的。

Code

void modify (int x, int y) { // 单点修改
if (x > n) { // 更新完毕
return ; // 返回
}
tr[x] += y, modify(x + lowbit(x), y); // 更新
}

复杂度 O(logn)

区间修改

开两个树状数组,利用差分维护即可。

本文作者:wnsyou の blog

本文链接:https://www.cnblogs.com/wnsyou-blog/p/seg_and_fenwick.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   wnsyou  阅读(53)  评论(0编辑  收藏  举报
  1. 1 勝利への道 安藤浩和
  2. 2 Minecraft’s End Eric Fullerton
  3. 3 月光曲完整版 贝多芬 云熙音乐
  4. 4 平凡之路 (Live版) 朴树
  5. 5 Minecraft C418
  6. 6 Paradise NiziU
  7. 7 叫我,灰原哀 龙大人不喷火
  8. 8 心机之蛙,一直摸你肚子 ——《名侦探柯南》原创同人曲 炊饭,叶辞樱,温海,寒砧,南柯柯,小茜玛姬,盛姝,阿崔Ac,贝壳初,千湛,兮茶子DaYu,乔慕,黎鹿北,起千温卿,遮阳伞,曲悠
  9. 9 战 歌 此去经年
叫我,灰原哀 - 龙大人不喷火
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 龙大人不喷火

作曲 : 纯哲不咕咕

编曲 : REVIVAL MUSIC

和声编写 : 胡佳弈

音源:灰原哀

这位就是从今天起要跟大家一起念书的灰原哀同学

什么嘛 摆一张臭脸

她好冷淡哦

我想她一定是紧张啦

我是海底逃出的鲨鱼

请多指教

不太礼貌 给你们警告

有我朋友出现的地方都会有人死掉

奇妙的 营造了

名侦探的名号

情报 是我 发出的信号

帮他 抓住这病号

就是 杀戮的殉道 者

你是否听到了

滴滴滴滴 炸弹的声音

听清叮咛 灵光乍现的分析 一半都靠我

当然不是小女孩 数理化信手都拈来

可御可萝不可得的女天才

丢失的挂件 达咩哟

抽丝的毛线 达咩哟

就拜托你帮忙开门 该死的静电 达咩哟

过时的行头 达咩哟

车内的停留 达咩哟

当爹当妈一级怪人 可怜的琴酒 达咩哟

重新开始希望也不会属于我

就像地平线送来朝阳又等待日落

天空走漏风声让我时刻保持沉默

黑色子弹划过 沉入夜色如墨

或许只有更沉的夜才更适合烟火

当我仰望天空在我眼中映下星河

这盛大的绚烂让我不再逃避闪躲

当我看向身侧 并非孤身一个

出谋划策 找寻下落

由我负责其中大多 数

小女孩也该有的酷

只有我能秀得住

就微笑看他 mz 枪发射

少年侦探队的家伙

或许应该对他们说

我的朋友 阿里嘎多

Baby 叫我 灰原哀

女孩心思 会难猜

我从反派反外 正义的板块 幕后的凶手被掩盖

就让我静静地等待着绯色的子弹壳穿过了他的胸膛

到最后那一刻或许我长大了不适合停留在谁的身旁

这些不会改变我

从来学不会软弱

看我 微笑的弧度 表演的无助

吸引更多人反侧

把你拐到梦想国度看焰火

我才是最完美的罪犯没有留下线索

丢失的挂件 达咩哟

抽丝的毛线 达咩哟

就拜托你帮忙开门 该死的静电 达咩哟

过时的行头 达咩哟

车内的停留 达咩哟

当爹当妈一级怪人 可怜的琴酒 达咩哟

重新开始希望也不会属于我

就像地平线送来朝阳又等待日落

天空走漏风声让我时刻保持沉默

黑色子弹划过 沉入夜色如墨

或许只有更沉的夜才更适合烟火

当我仰望天空在我眼中映下星河

这盛大的绚烂让我不再逃避闪躲

当我看向身侧 并非孤身一个

命运的天平我早就被放入了一边的托盘

另一边是药物枪火还有一把多兰

这首歌就要播完 或许不该有遮拦

最后一句就像生命我选择押在了柯南

嗯哼 我乱说的 干嘛这么认真啊

加载中…

{{tag.name}}

{{tran.text}}{{tran.sub}}
无对应文字
有可能是
{{input}}
尚未录入,我来提交对应文字
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示