复杂度分析
朴素分析
- 直接暴力统计计算次数。
摊还分析
聚合分析
-
核心思想:算出总复杂度然后均摊。
-
例:
-
1.“栈”:维护一个栈,支持栈顶插入和一次弹出所有元素。
-
显然 \(n\) 次操作至多 \(n\) 个点插入,从而最多有 \(n\) 个点弹出。
-
总复杂度 \(O(2n)\),均摊复杂度 \(O(2)\)。
-
-
2.\(vector\) 的复杂度分析(仅考虑队尾插入和申请空间的操作)。
-
按一般的算法,队尾插入 \(O(1)\),申请空间 \(O(size)\)。
-
显然至多申请 \(2^{\lceil log_2\ n\rceil}\)的空间,可以向上取到 \(O(2n)\)。然后插入 \(O(n)\),均摊复杂度 \(O(3)\)。
-
-
核算法
-
核心思想:在复杂度较低的操作上“预支”复杂度以防止负债。
-
注意,低复杂度操作的复杂度并没有真的升高,这种变换只是为了方便统计复杂度。
-
例:
- 1.\(vector\) 的复杂度分析(仅考虑队尾插入和申请空间的操作)。
-
按一般的算法,队尾插入 \(O(1)\),申请空间 \(O(size)\)。
-
我们考虑把队尾插入的复杂度上升到 \(O(3)\)。
-
从而上次开出的新点(恰为 \(\dfrac{size}{2}\) 个)每个都有 \(2\) 冗余复杂度。均摊到 \(size\) 个点,冗余为 \(1\),恰好够申请新一倍的空间。
-
即,总复杂度 \(O(3n)\),均摊复杂度 \(O(3)\)。
-
有趣的是,通过聚合分析算出的复杂度是往往达不到的,而核算法算出的复杂度是高度近似的(这里仅从计算原理来讲)。
-
然而如果我们把 \(n\) 取成 \(2\) 的整数次方的话,聚合分析可以得到 \(O(2)\),核算法仍然是 \(O(3)\)(最后存的很大一部分复杂度没有用到)。
-
这就启示我们,所有摊还分析本质上都是近似复杂度分析,并且常常向上取来保证不炸,从而精度未必非常良好。想要取得尽量精确的复杂度,需要选用正确的摊还方法。
-
- 1.\(vector\) 的复杂度分析(仅考虑队尾插入和申请空间的操作)。
势能分析
-
核心思想:
-
1.记当前状态为 \(D\),初始状态(通常为空)为 \(D_0\)。
-
2.定义势函数 \(\phi(D)\),使得总有 \(\phi(D)\geqslant \phi(D_0)\)。
-
3.记每种操作的实际代价为 \(c\),定义其摊还代价为 \(c'=c+\phi(now)-\phi(pre)\)。从而有 \(\dfrac{\sum\limits_{i=1}^n c'}{n}\) 给出了一种近似(一般略大于实际)的均摊复杂度。
-
-
例:
-
1.\(KMP\) 的复杂度分析。参见“字符串”。
-
2.\(splay\) 的复杂度分析。
-
主定理
-
其实不一定应该放在这里...
-
主定理给出了一种求解递归算法或分治算法的时间复杂度的方法。其具体形式如下:
-
定义 \(T(n)\) 为规模为 \(n\) 的问题在递归算法下的复杂度,且有 \(T(n)=aT(\dfrac{n}{b})+f(n)\),其中 \(f(n)\) 为规模为 \(n\) 的问题在分治后的合并复杂度。
-
设 \(f(n)=O(n^c),c'=\log_ba\)。则我们有:
\[T(n)=\begin{cases}f(n)=O(n^c),c>c'\\f(n)\times\log_bn=O(n^c\times\log_bn),c=c'\\ O(n^{c'}),c<c'\end{cases} \] -
-
可以简单地这么记忆:两个元素,合并复杂度与 \(n^{\log_b a}\);有主导看主导,没主导带个 \(\log_b a\)。