复杂度分析

复杂度分析

前言

\(O(x)\) 表示 \(x\) 的严格上界,\(\Omega(x)\) 表示 \(x\) 的严格下界,\(\Theta(x)\) 表示 \(x\) 的严格界(即严格上下界同阶)。

形式化地说,上界就是比值在无穷处为常数

\[y=O(x)\Leftrightarrow\exists k\in R^+,0<\lim_{n\rightarrow +\infty} \frac{y(n)}{x(n)}\le k \]

下界是类似的,

\[y=\Omega(x)\Leftrightarrow\exists k\in R^+,\lim_{n\rightarrow +\infty} \frac{y(n)}{x(n)}\ge k \]

两个都成立就是严格界

我们容易得到的是 \(\Theta(\log n)=\Theta(\ln n)=\Theta(\log_a n)\)

让人遗憾的是,人们在OI往往滥用了它们,严格来说,除了 \(O(1)\)\(\Theta(1)\) 可以无歧义的混用,其他地方都应该保持谨慎。(不过很多时候说算法我们都只关心上界,这个时候可以用 \(O\) 替代原本的 \(\Theta\))

严格界的分析

严格界的做法在 \(OI\) 中常见,但实际上没有想象中那样多(学界中一般是现有随机化的做法,再有去随机化的做法)

主定理

我们考虑解决一个常见的问题,求解如下的 \(T(n)\)

\(T(n)=aT(\frac nb)+f(n)\)
\(T(1)=O(1)\)

我们把递归树画出来(想象大雾

然后就没有递归的复杂度了,把每个点的调用复杂度加起来就是总复杂度

层数从 \(0\) 开始,每层有 \(a^i\) 个点,每个点的权值是 \(f(\frac {n}{b^i})\)

然后我们分类讨论

如果 \(f(n)=O(n^{\log_ba-\epsilon})\)

展开就可以得到 \(T(n)=O(n^{\log_ba})\)

同理,\(f(n)=O(n^{\log_ba})\)

可以得到 \(T(n)=O(n^{\log_ba}\log n)\)

\(f(n)=O(n^{\log_ba+\epsilon})\)

\(T(n)=O(f(n))\)

随机化

也就是所谓的“期望复杂度”

基于随机数据的随机算法

会被特殊的数据卡,但是在大多数数据上有优秀的表现,可以通过特殊的数据生成器

ODT

就是珂朵莉树

随机算法

和数据无关,基于一个随机数生成器产生的随机数是理想的(在值域上均匀)

均摊分析

势能分析

对于一类问题,某些操作可能会占用大量的时间,而另一些操作的代价则较小,导致整个算法的运行时间依然在可以接受的范围内。

举一个例子,一次加一个数或者删除前一半的数,压入是 \(\Theta(1)\) 的,但删除的复杂度是 \(\Theta(\frac{|s|}{2})\) 的,其中 \(|s|\) 表示当前的数据量。假如操作 \(n\) 次,看起来我们的上界是 \(O(n^2)\),但仔细想一想,一个数至多被加入和删除各一次,贡献为 \(\Theta(1)\),数的个数至多有 \(n\) 个,于是复杂度为 \(O(n)\)

对于类似的问题,一种更为泛用的方法是势能分析

联想物理中的保守力做功,我们往往习惯于定义一个势能去计算其贡献,再考虑其他部分,而这种势能往往是只和出初末状态相关的。

对于上面那个简单的例子,我们定义势函数 \(\phi=|s|\),操作 \(i\) 次后的势能为 \(\phi(i)\),总时间复杂度为 \(T(n)\)

则有

\[T(n)=\phi(n)-\phi(0)+\sum c(i)=\sum t(i) \]

其中 \(c(i)\)\(i\) 次操作的实际复杂度,\(t(i)\) 表示均摊复杂度

形式化地,我们有

\[t(i)=c(i)+\phi(i)-\phi(i-1)=c(i)+\Delta\phi \]

实际上,对于一次 \(\Theta(1)\) 的压入

\[\phi(i+1)=\phi(i)+1\\ t(i+1)_\text{insert}=O(1)+\Delta\phi=O(1) \]

实际上,如果一次插入是log的话会更容易看出中间 \(O(1)\) 项的作用。这告诉我们,一个算法即使是基于均摊的,其复杂度上界也不一定是由势能贡献的

对于一次删除

\[\phi(i+1)=\frac{\phi(i)}{2}\\ t(i+1)_\text{delete}=\frac{|s|}{2}+\phi(i+1)-\phi(i)=0 \]

之前“存储”在势能中的能量被释放出来,完成了一次操作。

Splay

容易说明 \(Splay\) 的复杂度就是 \(splay\) 操作的复杂度(\(splay\) 之后就可以 \(O(1)\) 直接操作点了)

不是一般性的,设 \(Splay\) 的大小和操作次数均为 \(\Theta(n)\)

一次 \(splay\) 可以分为 \(zig,zag,zig-zig,zig-zag,zag-zig,zag-zag\) 六种,考虑 \(zig,zag\) 是对称的,于是只需要分析 \(zig,zig-zig,zig-zag\)

记一个点的子树大小为 \(siz\)

对于这种较为一复杂的数据结构,我们考虑定义每个点的势能,整个结构的势能就是每个点的势能之和

设一个点的势能为 \(\varphi_{node}\)\(Splay\) 的势能为 \(\phi\),操作 \(i\) 次后为 \(\phi(i)\)

\[\varphi_{node}=\log_2siz_{node}\\ \phi=\sum_{node\in Splay}\varphi_{node} \]

显然有势能上界为 \(O(n\log n)\),下界为 \(\Omega(log^2n)\)

于是

\[T(n)=\phi(n)-\phi(0)+\sum c(i)=O(n\log n)+\sum c(i) \]

只需放缩出 \(\sum c(i)\) 的上界即可

\[t(i)=c(i)-\Delta\phi(i) \]

\[c(i)=O(1) \]

画图之后合理放缩(画图太麻烦了,可以看看这个

\(a(i)\) 表示一次旋转的复杂度

可以得到

\[a(i)\le 3(\varphi^\prime_{node}-\varphi_{node}) \]

对于一次 \(splay\),是由一段连续的旋转操作构成的,即

\[t(i)_{splay}=\sum_{j=a}^bO(\varphi^{(j)}_{node}-\varphi^{(j-1)_{node}})=O(\varphi^{last}_{node}-\varphi_node) \]

该节点初末势能的变化量,对于 \(\varphi_{node}\) 显然有其上界 \(O(\log n)\),下界 \(\Omega(1)\)

于是 \(t(i)_{splay}=O(\log n)\)

总的复杂度就是 \(O(n\log n)\)

LCT

\(LCT\) 的势能变化有链上的 \(Splay\) 和虚实边的变化两种,然后就发现所有操作都可以看做 \(access\)

依然假设树的大小和操作次数都为 \(\Theta(n)\)

还是考虑之前一样的做法

\[\varphi_{i}=\log siz_i\\ \phi=\sum\varphi_i \]

值得一提的,这里的 \(siz_i\) 表示所有虚边和实边连接的 \(i\) 的子树大小(也就是虚子树大小加 \(Splay\) 上的子树大小)

我们发现虽然 \(access\) 时,节点所属的 \(Splay\) 会改变,但这和直接合并成一条实链后再旋转没有区别(先不考虑跳虚边的复杂度),于是这一部分和 \(Splay\) 没有什么区别,依然是 \(O(n\log n)\)

再考虑虚实剖分的复杂度,和轻重剖分做比较,我们定义势函数为

\[\phi=\sum_{(u,v)\in LCT}[siz_u\le2siz_v\and fa_v=u\and son_u\ne v] \]

可以改写为更加显然的形式

\[\phi=\sum_{(u,v)\in LCT}[v\text{是}u\text{的重儿子}\and(u,v)\text{为虚边}] \]

轻重链剖分中,我们知道一个点到根的路径上至多有 \(O(\log n)\) 条轻边,自然至多产生 \(O(\log n)\) 条重虚边,而对于原本的重虚边,需要花费 \(\Theta(1)\) 的代价使之变为重实边,不会产生重虚边,于是这一部分的复杂度上界也为 \(O(n\log n)\)

至此就说明了 \(LCT\) 是均摊 \(O(n\log n)\)

posted @ 2022-04-22 21:19  嘉年华_efX  阅读(84)  评论(0编辑  收藏  举报