势能分析
引入
在某一类算法流程当中,中间流程十分复杂难以直接分析复杂度。
为了解决这类问题的复杂度分析,我们引入物理中能量守恒的思想来解决此问题。
其本质在于将复杂度刻画为做功引起势能变化,而由能量的特性只用管始末状态而不需要管中间的做工情况,恰好能解决开始所提到的中间流程复杂的问题。
一般地,使用势能分析得到的大多为均摊复杂度。
简介
一般模型如下(视情况也可调节):
令第 ii 次操作复杂度消耗为 ti,第 i 个状态为 Si,状态 S 所对应势能为 F(S),那么我们将 ti 描述为如下形式:
(可以将 ci 看作常量做功,F(Si)−F(Si−1) 看作势能变化量,ti 为总功)
那么我们有最终复杂度:
一般地,我们需要得出 ∑ci 的一个上界,以及 F(Sn)−F(S0) 的一个上界,以此确定 ∑ti 的一个上界。
简例
单调队列复杂度分析
事实上单调队列复杂度可以简单地分析,但为了熟悉势能分析法我们给出如下分析。
令 S 为单调队列的一个状态,F(S)=|S|,ti 为第 i 次操作消耗复杂度,ci 为第 i 次操作入队数量,Si 为第 i 次操作结束后(出队结束后)单调队列的状态。
那么有:
则总复杂度为:
因此我们也从势能分析的角度得到了单调队列复杂度的证明。
并查集启发式合并复杂度分析
约定
我们记节点 x 的子树大小为 |x|,其势能函数 φ(x)=log|x|。
显然并查集只存在两种操作,于是分两种操作考虑复杂度。
- find 操作
显然单次复杂度 O(1),但由于我们不知道调用次数上限,因此只能将其放缩为势能变化的形式(不带常数)。
具体地,我们称简介中的第 i 个状态为 find 操作中访问到的第 i 个节点 x 那么下一个节点即为 x′=fax 为第 i+1 个状态。
那么就需要将此常数放缩成 k(φ(x′)−φ(x)) 的形式。
由于启发式合并的特性,我们有:
可知:
那么有单次复杂度变化量:
因此有 find 操作总复杂度:
- Merge 操作
显然 Merge 操作由两个 find 操作及一次 O(1) 操作构成,由 1 的分析可知复杂度为 O(logn)。
Splay 复杂度分析
约定
记小写字母 x 为 Splay 上的一个节点,x′ 为 x 经过操作变换后对应节点,大写字母 S 为一颗 Splay。
记 y 为 x 在 Splay 上的父亲,z 为 y 在 Splay 上的父亲,|x| 为节点 x 在 Splay 上子树的大小。
定义节点 x 的势能函数 φ(x)=log|x|,Splay S 的势能函数为 ϕ(S)=∑x∈Sφ(x)。
下面我们证明 Splay 单次将节点 x 旋到根的复杂度为 O(logn) 其他操作均可视为该操作的组合。
对于 Splay 双旋的两种情况我们考虑分别将复杂度增量描述为势能变化量。
同时,由于单次旋转复杂度 O(1) 很难描述为势能变化量,因此我们考虑将单次旋转复杂度描述为:O(1)+Δϕ(S)
得到两者和的上界再考虑减去 ϕ(S) 总变化量下界即可,由定义可知,总变化量:
因此可以将修改后计算出的复杂度加上 nlogn 即可,不影响总复杂度。
- 若 x,y,z 三点共线
有总势能变化量:
又我们将复杂度描述为 O(1)+Δϕ(S),我们并不好衡量常数之和,因此尽量将其放缩为不存在常量的势能变化量。
显然不能将上界放缩成 Δϕ(S) 的形式,因此只能将上界放缩为 k(φ(x′)−φ(x)),k 为常数的形式。
据此,考虑将复杂度表达式放缩引入 φ(x′) 并尽可能化简式子。
同时,为了方便我们直接将 O(1) 看作 1,最后直接乘该常量即可。
考虑借助势能关系放缩 1,我们有:
进一步有:
带入原式:
旋转结束后,可得总复杂度 <3(φ(root)−φ(x0))<3logn 得证。
- 若 x,y,z 三点不共线
同样有:
注意,此时我们所拥有的条件变化,因此不像上一种情况一样放缩。
具体地,我们有:
类似地,可以得到:
那么有:
旋转结束后,可得总复杂度 <2(φ(root)−φ(x0))<2logn 得证。
由此,我们得到了 Splay 的复杂度为 O((n+m)logn)。
LCT 复杂度分析
显然 LCT 复杂度只来源于 Access 操作,而 Access 操作的复杂度来源又可以分为如下两个部分:
- 虚实儿子切链
- 实链上 Splay
由于这两个部分互不干扰,因此将复杂度分析分开考虑。
虚实儿子切链复杂度
首先将树进行重链剖分,称既为原树重边也为 LCT 中虚边的边成为重虚边,其余类似。
设置整体势能函数 ϕ(S) 为 LCT:S 中的 重虚边 数量。
考虑 Access 过程中势能函数的变化,有如下两种:
- 势能函数增加 1,当且仅当当前切换链为 轻虚边,原实边为重边。由重链剖分理论,单次 Access 该情况次数不超过 logn。
- 势能函数减少 1,当且仅当当前切换链为 重虚边,原实边为轻边。
由 1 我们有:
因为有势能函数非负,因此有:
显然总复杂度:
因此切链复杂度是 O(n+mlogn) 的。
实链 Splay 复杂度
沿用 Splay 复杂度的分析方式,假设在第 i 条实链内部 Splay 复杂度为 k(φ(rooti)−φ(x0i))。
显然有:
因此总复杂度:
结合两者,有 LCT 复杂度 O((n+m)logn)
基本分析方式
结合上述四个经典势能分析,我们可以发现势能分析应用时的两种情况:
-
复杂度证明并不依赖于势能变化量(证明不依赖于操作对势能的影响),大多数情况下可以被一般方法代替,例如 1,2。
-
复杂度证明强依赖于势能变化量(证明依赖于操作对势能的影响),一般这类情况是势能分析的专场,例如 3,4。
对于 1,若需要进行势能分析,往往 只需要 将复杂度描述为势能变化量再考虑始末势能变化量即可,且多数情况下该变化量与操作无关,只于实际问题上界有关。
对于 2,需要我们在复杂度描述中引入势能总体变化量,因为这样才方便势能的摊还。
具体来说,将进行单次操作或可简单分析上界的操作对总势能贡献为 +,将多次操作或无法分析上界的操作对总势能贡献为 −。
这样根据势能函数的非负性,可以得到贡献 − 的操作复杂度与贡献 + 的操作复杂度之间的关系从而进行求解,例如 LCT 中切链复杂度的分析。
还有一种比较复杂度的情况,通过引入整体势能函数无法简单证明,这时我们就需要引入局部势能函数。
一般对于 后者 描述为势能变化量的形式以放缩开始引入的整体势能函数分析单次操作复杂度。
简单练习
二进制加法
模拟二进制下的加法,从 0 开始不断 +1 直到 n,每次的操作为 O(1) 修改一个位置的数。
显然加法过程中 01 有别,因此我们将二进制数划分成极大的 01 段。
一种简单的想法是令势能函数为这样的段数,但发现无法用段数描述复杂度。
于是考虑修改划分方式,我们将一个单独的 0 看作一段,极长的 1 还是看作一段。
令 φ(x) 为数字 x 以上述方式划分的段数,分析一下各种情况下 φ(x) 的变化量:
- 形如:⋯1⋯10 则易知 φ(x+1)=φ(x)−1
- 形如:⋯00 易知 φ(x+1)=φ(x)
- 形如:⋯001⋯1(最后一段 1 的个数为 k)易知 φ(x+1)=φ(x)+k−1
- 形如:⋯1⋯101⋯1(最后一段 1 的个数为 k)易知 φ(x+1)=φ(x)+k−2
接下来存在两种证法:
直接利用势能变化量放缩复杂度
可知上述四种情况复杂度依次为:O(1),O(1),O(k+1),O(k+1)
则我们有单次复杂度的一个上界:φ(S+1)−φ(S)+3,因此有总复杂度:
借助势能函数有界性
观察势能函数的减少量,可知单次至多减少 1,同时任意时刻我们有界:
假设复杂度增加量之和为:ΔT+≥0 减少量 绝对值 之和为 ΔT−≥0,那么有:
由于减少量之和 ΔT−≤n 因此有总复杂度:
并查集路径压缩
还不会,先咕着......
并查集路径压缩 + 启发式合并
也不会,先咕着......
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库