势能线段树学习笔记

本文介绍了势能线段树在处理区间最值、区间历史最值和区间第K小值等问题的应用,涉及更新策略、时间复杂度分析以及与常见IT题目类型的关联。

势能线段树


也叫吉司机线段树


用途

区间最值操作

给你一个长度为 n n n 的序列,有 Q Q Q 次操作,操作类型如下:

0 u v t :对于所有 i i i , $u \leq i \leq v $ , min ⁡ ( a i , t ) → a i \min(a_i,t) \to a_i min(ai,t)ai

1 u v :对于所有 i i i , $u \leq i \leq v $ , 输出最大的 a i a_i ai

2 u v :对于所有 i i i , $u \leq i \leq v $ , 输出的 a i a_i ai 的和。

对于 12 ,在学普通的线段树时就应该掌握,而他们也确实不是今天的主角。

看到 0 ,怎么维护?

这就要用到势能线段树了。

我们在线段树中维护区间的最大值,最大值个数, 严格次大值(一定小于最大值),当然,题目还要求总和,所以这个也要加上。

那我们如何更新呢?

0 出现时,我们进入线段树,会出现三种情况:

  1. 当一个区间的最大值小于等于 t t t 时,直接退出,因为该操作肯定不会对它造成影响;
  2. 当一个区间的最大值大于 t t t ,而严格次大值小于 t t t 时,我们只要拿最大值出来更新就可以了;
  3. 当一个区间的严格次大值大于等于 t t t 时,我们让它暴力扫下去,也就是让它的子区间去更新。

有人会疑惑了:“这怎么保证时间复杂度?”

确实,这是一个令人困惑的问题,但是可以仔细想一下:

我们将一个区间内不同数字的种类数叫做 势能 ,那么一整棵线段树上的势能总和最大为 n log ⁡ 2 n n\log_2{n} nlog2n ,而我们每做一次暴力修改,整体势能会至少减一,那么暴力执行最多 n log ⁡ 2 n n\log_2{n} nlog2n 次,时间复杂度也就是 O ( n log ⁡ 2 n ) O(n\log_2{n}) O(nlog2n)

这也是为什么它叫做势能线段树

不过,这是不带其他种类的修改的情况,带上修改后时间复杂度就变成了 O ( n log ⁡ 2 2 n ) O(n\log_2^2{n}) O(nlog22n)

这只是势能线段树的其中一种用途,叫做区间最值操作,除了最小值还有最大值,不过都是一样的。

区间历史最值

给你一个长度为 n n n 的序列,有 Q Q Q 次操作,操作类型如下:

0 u v t :对于所有 i i i , $u \leq i \leq v $ , a i a_i ai 加上 t t t

1 u v :对于所有 i i i , $u \leq i \leq v $ , 输出最大的 a i a_i ai​ ;

2 u v :对于所有 i i i , $u \leq i \leq v $ , 输出最大的 a i a_i ai 达到过的值;

那么我们又该怎么维护这个东东呢?

我们在线段树中维护区间的最大值 m x mx mx,历史最大值 h m x hmx hmx前缀加和 s s s ,从上一次 pushdown 操作后最大的前缀加和 m s ms ms

然后在操作时,最重要的是 pushup 和 pushdown ,

对于这三个简单操作,pushup 并不难,pushdown 才是重点。

在 pushdown 的时候,我们要做一个

tr[son].hmx=max(tr[son].hmx,tr[son].mx+tr[pa].ms)tr[son].ms=max(tr[son].ms,tr[son].s+tr[pa].sm)

就相当于把父节点上的操作接到了子节点上,我们可以用两个队列相接来理解。

这样就可以保证区间历史最值的更新了。

区间历史最值还可以与区间赋值等一坨东西结合起来,总之很烦。

区间第 K K K 小值

这个属实是我没想到的…

给你一个长度为 n n n 的序列,有 Q Q Q 次操作,操作类型如下:

0 u v t :对于所有 i i i , $u \leq i \leq v $ , min ⁡ ( a i , t ) → a i \min(a_i,t) \to a_i min(ai,t)ai

1 u v k :对于所有 i i i , $u \leq i \leq v $ , 输出第 k k k 小的 a i a_i ai

基础的区间第 K K K 小值查询就是在权值线段树上二分,找到那个值,这里也是一样。

在修改时,外层的线段树可以打上取 min ⁡ \min min 的标记,但这里有一个问题:它无法实现快速的标记下传与修改。

这个时候,我们可以把相同的数放到一起修改,这就体现了势能线段树的思想。

修改时,对于外层线段树上拆出的 log ⁡ 2 n \log_2^n log2n 个节点,找出这个节点上所有本质不同的大于 t t t 的数,在这个节点及其祖先上把这些数改为 t t t 即可。

我们发现把每个节点内本质不同的数的个数减一,都要在 log ⁡ 2 n \log_2{n} log2n 棵内层线段树上修改,时间复杂度直接到了 O ( log ⁡ 2 2 n ) O(\log_2^2{n}) O(log22n)

总复杂度为 $ O((n+Q)\log_2^3{n})$​ 。

详见 [学习笔记]吉司机线段树 - epic01 - 博客园 (cnblogs.com).

关于这个用途,我找不到题源(难受)。


模版题

区间最值操作

  1. Gorgeous Sequence - HDU 5306 - Virtual Judge (vjudge.net):区间赋最小值,区间和。
  2. 最假女选手 - 黑暗爆炸 4695 - Virtual Judge (vjudge.net):区间赋最小,大值,区间加,区间和,区间求最大,小值。
  3. U180387 CTSN loves segment tree - 洛谷 | 计算机科学教育新生态 (luogu.com.cn):双数组结合,区间赋最小值,区间加。
  4. Mzl loves segment tree:区间赋最小,大值,区间加。

(3,4题详见区间最值操作 & 区间历史最值 - OI Wiki (oi-wiki.org))

区间历史最值

  1. CPU 监控 - 洛谷 P4314 - Virtual Judge (vjudge.net):区间加,区间覆盖,历史最大值。
  2. Good Subsegments - CodeForces 997E - Virtual Judge (vjudge.net):历史区间最小值,历史区间最小值个数

混合题目

  1. 线段树 3(区间最值操作、区间历史最值) - 洛谷 P6242 - Virtual Judge (vjudge.net)

相关资料

  1. 线段树历史最值问题 - Flandre-Zhu - 博客园 (cnblogs.com)
  2. [学习笔记]吉司机线段树 - epic01 - 博客园 (cnblogs.com)
  3. 区间最值操作 & 区间历史最值 - OI Wiki (oi-wiki.org)

平衡树是一种数据结构,旨在通过维护树的平衡性来确保操作的高效性。在平衡树中,势能分析常用于分析某些特定平衡树结构(如Splay树和替罪羊树)的均摊复杂度。这类平衡树的特点在于,单次操作的时间复杂度可能是不稳定的(例如,某些操作可能需要 $ O(1) $ 时间,而另一些可能需要 $ O(n) $ 时间),但通过势能分析可以证明,整体操作序列的复杂度均摊为 $ O(\log n) $。 对于Splay树而言,其核心思想是通过将最近访问的节点移动到树根来优化后续访问的性能。这种操作被称为“伸展”(splaying)。虽然单次伸展操作的最坏时间复杂度可能达到 $ O(n) $,但通过势能分析可以证明,一系列 $ n $ 次操作的总时间复杂度为 $ O(n \log n) $。势能分析的关键在于引入一个势能函数,用于衡量树的不平衡程度。常见的势能函数定义为树中所有节点的深度之和。通过对每次操作前后势能的变化进行分析,可以证明伸展操作的均摊时间复杂度是 $ O(\log n) $。 替罪羊树(Scapegoat Tree)则是一种基于重构的平衡树结构。其核心思想是当树的不平衡程度超过某个阈值时,选择一个“替罪羊”节点并将其子树重构为完全二叉树。虽然重构操作的时间复杂度为 $ O(k) $(其中 $ k $ 是子树的大小),但通过势能分析可以证明,这种重构操作的均摊复杂度仍然是 $ O(\log n) $。势能分析在此类结构中的作用在于,通过量化树的不平衡程度,能够证明重构操作的总代价在整个操作序列中是均摊高效的。 势能分析的核心在于势能函数的设计。对于平衡树结构,势能函数通常与树的高度或节点分布有关。例如,在Splay树中,势能函数可以定义为所有节点的秩(rank)的对数之和,其中秩可以是子树的大小。通过对势能函数的变化进行分析,可以得出每次操作的均摊代价,并最终证明整体复杂度的稳定性。 以下是一个简单的Splay树实现的伪代码,展示了伸展操作的基本思想: ```python class SplayTreeNode: def __init__(self, key): self.key = key self.left = None self.right = None self.parent = None class SplayTree: def __init__(self): self.root = None def splay(self, node): while node.parent is not None: if node.parent.parent is None: # Zig step self.zig(node) else: # Zig-zig or Zig-zag if node == node.parent.left and node.parent == node.parent.parent.left: self.zig_zig(node) else: self.zig_zag(node) self.root = node def zig(self, node): # Rotate right parent = node.parent grandparent = parent.parent node.right, parent.left = parent, node.right parent.parent = node node.parent = grandparent if grandparent is not None: if grandparent.left == parent: grandparent.left = node else: grandparent.right = node def zig_zig(self, node): # Rotate right twice parent = node.parent grandparent = parent.parent great_grandparent = grandparent.parent node.right, parent.left = parent, node.right parent.right, grandparent.left = grandparent, parent.right grandparent.parent = parent parent.parent = node node.parent = great_grandparent if great_grandparent is not None: if great_grandparent.left == grandparent: great_grandparent.left = node else: great_grandparent.right = node def zig_zag(self, node): # Rotate left then right parent = node.parent grandparent = parent.parent node.left, grandparent.right = grandparent, node.left node.right, parent.left = parent, node.right parent.parent = node grandparent.parent = node node.parent = None def insert(self, key): # Insert logic here pass def search(self, key): # Search logic here pass ``` 通过势能分析,可以证明Splay树和替罪羊树的均摊复杂度为 $ O(\log n) $。这种方法不仅适用于这些特定的平衡树结构,也为其他数据结构的均摊复杂度分析提供了理论基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值