OSDN Git Service

将复杂度中非指数的 log_2 替换为 log
authorouuan <y___o___u@126.com>
Sun, 25 Aug 2019 10:02:14 +0000 (18:02 +0800)
committerouuan <y___o___u@126.com>
Sun, 25 Aug 2019 10:02:14 +0000 (18:02 +0800)
22 files changed:
docs/ds/block-array.md
docs/ds/bst.md
docs/ds/cartesian-tree.md
docs/ds/divide-combine.md
docs/ds/kdt.md
docs/ds/lct.md
docs/ds/persistent-heap.md
docs/ds/persistent-in-bit.md
docs/ds/queue.md
docs/ds/seg-beats.md
docs/ds/seg.md
docs/ds/sqrt-tree.md
docs/ds/treap.md
docs/graph/dynamic-tree-divide.md
docs/graph/kth-path.md
docs/graph/prufer.md
docs/graph/tree-divide.md
docs/math/inclusion-exclusion-principle.md
docs/math/matrix.md
docs/math/quick-pow.md
docs/misc/complexity.md
docs/misc/parallel-binsearch.md

index 763bed9..84cb4ae 100644 (file)
@@ -23,7 +23,7 @@ for (int i = 1; i <= num; i++) {
 
 ### 例题 1: [教主的魔法](https://www.luogu.org/problemnew/show/P2801) 
 
-我们要询问一个块内大于等于一个数的数的个数,所以需要一个 `t` 数组对块内排序。对于整块的修改,使用类似于标记永久化的方式保存。时间复杂度 $O(n\sqrt{n}\times log_2{\sqrt{n}})$ 
+我们要询问一个块内大于等于一个数的数的个数,所以需要一个 `t` 数组对块内排序。对于整块的修改,使用类似于标记永久化的方式保存。时间复杂度 $O(n\sqrt{n}\log n)$ 
 
 ```cpp
 void Sort(int k) {
index a009e03..a1bccd8 100644 (file)
@@ -10,7 +10,7 @@
 
 4.  二叉搜索树的左右子树均为二叉搜索树。
 
-二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 $n$ 个结点的二叉搜索树中,这些操作的最优时间复杂度为 $O(\log_2 n)$ ,最坏为 $O(n)$ 。随机构造这样一棵二叉搜索树的期望高度为 $O(\log_2 n)$ 。
+二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 $n$ 个结点的二叉搜索树中,这些操作的最优时间复杂度为 $O(\log n)$ ,最坏为 $O(n)$ 。随机构造这样一棵二叉搜索树的期望高度为 $O(\log n)$ 。
 
 ## 基本操作
 
index f3cac4e..6ca4ba9 100644 (file)
@@ -12,7 +12,7 @@
 
 ## 构建
 
-既然笛卡尔树有具有两种不同结构的性质,那么就有两种构建方法。其中一种的复杂度是 $O(n)$ ,另一种是 $O(n\log_2n)$ 。
+既然笛卡尔树有具有两种不同结构的性质,那么就有两种构建方法。其中一种的复杂度是 $O(n)$ ,另一种是 $O(n\log n)$ 。
 
 ### 栈构建
 
@@ -26,7 +26,7 @@
 
 ## 另一种构建
 
-还有一种构建方式与之对应。我们按照 $w$ 排序,始终维护堆的结构。这样插入一个结点,相当于添加一个叶子结点。于是我们像平衡树那样插入就行了。这样每一次插入的复杂度是 $O(\log_2n)$ 的,因此总复杂度是 $O(n\log_2n)$ 的。
+还有一种构建方式与之对应。我们按照 $w$ 排序,始终维护堆的结构。这样插入一个结点,相当于添加一个叶子结点。于是我们像平衡树那样插入就行了。这样每一次插入的复杂度是 $O(\log n)$ 的,因此总复杂度是 $O(n\log n)$ 的。
 
 ## 笛卡尔树与 Treap
 
index 13f4bef..0176cdf 100644 (file)
@@ -100,7 +100,7 @@ $$
 
 ### 析合树的构造
 
-前面讲了这么多零零散散的东西,现在就来具体地讲如何构造析合树。LCA 大佬的线性构造算法我是没看懂的,今天就讲一下比较好懂的 $O(n\log_2n)$ 的算法。
+前面讲了这么多零零散散的东西,现在就来具体地讲如何构造析合树。LCA 大佬的线性构造算法我是没看懂的,今天就讲一下比较好懂的 $O(n\log n)$ 的算法。
 
 #### 增量法
 
index 9aa3e1d..80202d5 100644 (file)
@@ -36,13 +36,13 @@ k-D Tree 具有二叉搜索树的形态,二叉搜索树上的每个结点都
 
 2.  每次在维度上选择切割点时选择该维度上的 **中位数** ,这样可以保证每次分成的左右子树大小尽量相等。
 
-可以发现,使用优化 $2$ 后,构建出的 k-D Tree 的树高最多为 $O(\log_2 n)$ 。
+可以发现,使用优化 $2$ 后,构建出的 k-D Tree 的树高最多为 $O(\log n)$ 。
 
-现在,构建 k-D Tree 时间复杂度的瓶颈在于快速选出一个维度上的中位数,并将在该维度上的值小于该中位数的置于中位数的左边,其余置于右边。如果每次都使用 `sort` 函数对该维度进行排序,时间复杂度是 $O(n\log_2^2 n)$ 的。事实上,单次找出 $n$ 个元素中的中位数并将中位数置于排序后正确的位置的复杂度可以达到 $O(n)$ 。
+现在,构建 k-D Tree 时间复杂度的瓶颈在于快速选出一个维度上的中位数,并将在该维度上的值小于该中位数的置于中位数的左边,其余置于右边。如果每次都使用 `sort` 函数对该维度进行排序,时间复杂度是 $O(n\log^2 n)$ 的。事实上,单次找出 $n$ 个元素中的中位数并将中位数置于排序后正确的位置的复杂度可以达到 $O(n)$ 。
 
-我们来回顾一下快速排序的思想。每次我们选出一个数,将小于该数的置于该数的左边,大于该数的置于该数的右边,保证该数在排好序后正确的位置上,然后递归排序左侧和右侧的值。这样的期望复杂度是 $O(n\log_2 n)$ 的。但是由于 k-D Tree 只要求要中位数在排序后正确的位置上,所以我们只需要递归排序包含中位数的 **一侧** 。可以证明,这样的期望复杂度是 $O(n)$ 的。在 `algorithm` 库中,有一个实现相同功能的函数 `nth_element()` ,要找到 `s[l]` 和 `s[r]` 之间的值按照排序规则 `cmp` 排序后在 `s[mid]` 位置上的值,并保证 `s[mid]` 左边的值小于 `s[mid]` ,右边的值大于 `s[mid]` ,只需写 `nth_element(s+l,s+mid,s+r+1,cmp)` 。
+我们来回顾一下快速排序的思想。每次我们选出一个数,将小于该数的置于该数的左边,大于该数的置于该数的右边,保证该数在排好序后正确的位置上,然后递归排序左侧和右侧的值。这样的期望复杂度是 $O(n\log n)$ 的。但是由于 k-D Tree 只要求要中位数在排序后正确的位置上,所以我们只需要递归排序包含中位数的 **一侧** 。可以证明,这样的期望复杂度是 $O(n)$ 的。在 `algorithm` 库中,有一个实现相同功能的函数 `nth_element()` ,要找到 `s[l]` 和 `s[r]` 之间的值按照排序规则 `cmp` 排序后在 `s[mid]` 位置上的值,并保证 `s[mid]` 左边的值小于 `s[mid]` ,右边的值大于 `s[mid]` ,只需写 `nth_element(s+l,s+mid,s+r+1,cmp)` 。
 
-借助这种思想,构建 k-D Tree 时间复杂度是 $O(n\log_2 n)$ 的。
+借助这种思想,构建 k-D Tree 时间复杂度是 $O(n\log n)$ 的。
 
 ## 插入/删除
 
@@ -54,7 +54,7 @@ k-D Tree 具有二叉搜索树的形态,二叉搜索树上的每个结点都
 
 如果还有删除操作,则使用 **惰性删除** ,即删除一个结点时打上删除标记,而保留其在 k-D Tree 上的位置。如果这样写,当未删除的结点数在以 $x$ 为根的子树中的占比小于 $\alpha$ 时,同样认为这个子树是不平衡的,需要重构。
 
-类似于替罪羊树,带重构的 k-D Tree 的树高仍然是 $O(\log_2 n)$ 的。
+类似于替罪羊树,带重构的 k-D Tree 的树高仍然是 $O(\log n)$ 的。
 
 ## 邻域查询
 
@@ -265,7 +265,7 @@ k-D Tree 具有二叉搜索树的形态,二叉搜索树上的每个结点都
 
 在查询矩形区域内的所有点的权值和时,仍然需要记录子树内每一维度上的坐标的最大值和最小值。如果当前子树对应的矩形与所求矩形没有交点,则不继续搜索其子树;如果当前子树对应的矩形完全包含在所求矩形内,返回当前子树内所有点的权值和;否则,判断当前点是否在所求矩形内,更新答案并递归在左右子树中查找答案。
 
-已经证明,如果在 $2-D$ 树上进行矩阵查询操作,已经被完全覆盖的子树不会继续查询,则单次查询时间复杂度是最优 $O(\log_2 n)$ ,最坏 $O(\sqrt n)$ 的。将结论扩展到 $k$ 维的情况,则最坏时间复杂度是 $O(n^{1-\frac 1 k})$ 的。
+已经证明,如果在 $2-D$ 树上进行矩阵查询操作,已经被完全覆盖的子树不会继续查询,则单次查询时间复杂度是最优 $O(\log n)$ ,最坏 $O(\sqrt n)$ 的。将结论扩展到 $k$ 维的情况,则最坏时间复杂度是 $O(n^{1-\frac 1 k})$ 的。
 
 ??? "参考代码"
     ```cpp
index af1c887..f73d37a 100644 (file)
@@ -381,7 +381,7 @@ inline int Find(int p) {
 
 ## 维护树链信息
 
-LCT 通过 `Split(x,y)` 操作,可以将树上从点 $x$ 到点 $y$ 的路径提取到以 $y$ 为根的 Splay 内,树链信息的修改和统计转化为平衡树上的操作,这使得 LCT 在维护树链信息上具有优势。此外,借助 LCT 实现的在树链上二分比树链剖分少一个 $O(\log_2 n)$ 的复杂度。
+LCT 通过 `Split(x,y)` 操作,可以将树上从点 $x$ 到点 $y$ 的路径提取到以 $y$ 为根的 Splay 内,树链信息的修改和统计转化为平衡树上的操作,这使得 LCT 在维护树链信息上具有优势。此外,借助 LCT 实现的在树链上二分比树链剖分少一个 $O(\log n)$ 的复杂度。
 
 ???+note " 例题[luogu P1501\[国家集训队\]Tree II](https://www.luogu.org/problemnew/show/P1501)"
     给出一棵有 $n$ 个结点的树,每个点的初始权值为 $1$ 。 $q$ 次操作,每次操作均为以下四种之一:
index bd8ea72..c783ecf 100644 (file)
@@ -16,7 +16,7 @@
 
 4.  维护当前合并后左偏树的左偏性质,维护 `dist` 值,返回选择的根节点。
 
-由于每次递归都会使 `dist[x]+dist[y]` 减少一,而 `dist[x]` 是 $O(\log_2 n)$ 的,一次最多只会修改 $O(\log_2 n)$ 个结点,所以这样做的时间复杂度是 $O(\log_2 n)$ 的。
+由于每次递归都会使 `dist[x]+dist[y]` 减少一,而 `dist[x]` 是 $O(\log n)$ 的,一次最多只会修改 $O(\log n)$ 个结点,所以这样做的时间复杂度是 $O(\log n)$ 的。
 
 可持久化要求保留历史信息,使得之后能够访问之前的版本。要将左偏树可持久化,就要将其沿途修改的路径复制一遍。
 
@@ -30,7 +30,7 @@
 
 4.  维护以 $p$ 为根的左偏树的左偏性质,维护其 `dist` 值,返回 $p$ 。
 
-由于左偏树一次最多只会修改并新建 $O(\log_2 n)$ 个结点,设操作次数为 $m$ ,则可持久化左偏树的时间复杂度和空间复杂度均为 $O(m\log_2 n)$ 。
+由于左偏树一次最多只会修改并新建 $O(\log n)$ 个结点,设操作次数为 $m$ ,则可持久化左偏树的时间复杂度和空间复杂度均为 $O(m\log n)$ 。
 
 ### 参考实现
 
index 9de7699..1b0f309 100644 (file)
@@ -1,4 +1,4 @@
- [静态区间 k 小值 (POJ 2104 K-th Number)](http://poj.org/problem?id=2104) 的问题可以用 [主席树](./persistent-seg.md) 在 $O(n\log_2 n)$ 的时间复杂度内解决。
+ [静态区间 k 小值 (POJ 2104 K-th Number)](http://poj.org/problem?id=2104) 的问题可以用 [主席树](./persistent-seg.md) 在 $O(n\log n)$ 的时间复杂度内解决。
 
 如果区间变成动态的呢?即,如果还要求支持一种操作:单点修改某一位上的值,又该怎么办呢?
 
@@ -6,7 +6,7 @@
 
 ??? note " 例题[ZOJ 2112 Dynamic Rankings](http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1112)"
 
-如果用 [线段树套平衡树](./balanced-in-seg.md) 中所论述的,用线段树套平衡树,即对于线段树的每一个节点,对于其所表示的区间维护一个平衡树,然后用二分来查找 $k$ 小值。由于每次查询操作都要覆盖多个区间,即有多个节点,但是平衡树并不能多个值一起查找,所以时间复杂度是 $O(n\log_2^3 n)$ ,并不是最优的。
+如果用 [线段树套平衡树](./balanced-in-seg.md) 中所论述的,用线段树套平衡树,即对于线段树的每一个节点,对于其所表示的区间维护一个平衡树,然后用二分来查找 $k$ 小值。由于每次查询操作都要覆盖多个区间,即有多个节点,但是平衡树并不能多个值一起查找,所以时间复杂度是 $O(n\log^3 n)$ ,并不是最优的。
 
 思路是,把二分答案的操作和查询小于一个值的数的数量两种操作结合起来。最好的方法是使用 **线段树套主席树** 。
 
@@ -14,9 +14,9 @@
 
 思路类似于线段树套平衡树,即对于线段树所维护的每个区间,建立一个动态开点权值线段树,表示其所维护的区间的值。
 
-在修改操作进行时,先在线段树上从上往下跳到被修改的点,删除所经过的点所指向的动态开点权值线段树上的原来的值,然后插入新的值,要经过 $O(\log_2 n)$ 个线段树上的节点,在动态开点权值线段树上一次修改操作是 $O(\log_2 n)$ 的,所以修改操作的时间复杂度为 $O(\log_2^2 n)$ 。
+在修改操作进行时,先在线段树上从上往下跳到被修改的点,删除所经过的点所指向的动态开点权值线段树上的原来的值,然后插入新的值,要经过 $O(\log n)$ 个线段树上的节点,在动态开点权值线段树上一次修改操作是 $O(\log n)$ 的,所以修改操作的时间复杂度为 $O(\log^2 n)$ 。
 
-在查询答案时,先取出该区间覆盖在线段树上的所有点,然后用类似于静态区间 $k$ 小值的方法,将这些点一起向左儿子或向右儿子跳。如果所有这些点左儿子存储的值大于等于 $k$ ,则往左跳,否则往右跳。由于最多只能覆盖 $O(\log_2 n)$ 个节点,所以最多一次只有这么多个节点向下跳,时间复杂度为 $O(\log_2^2 n)$ 。
+在查询答案时,先取出该区间覆盖在线段树上的所有点,然后用类似于静态区间 $k$ 小值的方法,将这些点一起向左儿子或向右儿子跳。如果所有这些点左儿子存储的值大于等于 $k$ ,则往左跳,否则往右跳。由于最多只能覆盖 $O(\log n)$ 个节点,所以最多一次只有这么多个节点向下跳,时间复杂度为 $O(\log^2 n)$ 。
 
 由于线段树的常数较大,在实现中往往使用常数更小且更方便处理前缀和的 **树状数组** 实现。
 
index 3592cf2..2537db3 100644 (file)
@@ -72,7 +72,7 @@ LOJ6515 贪玩蓝月
 
 每个二元组是有一段存活时间的,因此对时间建立线段树,每个二元组做 log 个存活标记。因此我们要做的就是对每个询问,求其到根节点的路径上的标记的一个最优子集。显然这个可以 DP 做。 $f[S,j]$ 表示选择集合 S 中的物品余数为 j 的最大价值。(其实实现的时侯是有序的,直接 f[i,j]做)
 
-一共有 $O(m\log_2m)$ 个标记,因此这么做的话复杂度是 $O(mp\log_2m)$ 的。
+一共有 $O(m\log m)$ 个标记,因此这么做的话复杂度是 $O(mp\log m)$ 的。
 
 ### 在线算法
 
index 6f32901..dd78522 100644 (file)
@@ -20,7 +20,7 @@
 2.  如果 $Se<t\le Max$ ,那么这个 $t$ 就能更新当前区间中的最大值。于是我们让区间和加上 $Cnt(t-Max)$ ,然后更新 $Max$ 为 $t$ ,并打一个标记。
 3.  如果 $t\le Se$ ,那么这时你发现你不知道有多少个数涉及到更新的问题。于是我们的策略就是,暴力递归向下操作。然后上传信息。
 
-这个算法的复杂度如何?使用势能分析法可以得到复杂度是 $O(m\log_2 n)$ 的。具体分析过程见论文。
+这个算法的复杂度如何?使用势能分析法可以得到复杂度是 $O(m\log n)$ 的。具体分析过程见论文。
 
 ```cpp
 #include <algorithm>
@@ -316,7 +316,7 @@ int main() {
 }
 ```
 
-吉老师证出来这个算法的复杂度是 $O(m\log_2^2 n)$ 的。
+吉老师证出来这个算法的复杂度是 $O(m\log^2 n)$ 的。
 
 ### Mzl loves segment tree
 
@@ -347,7 +347,7 @@ int main() {
 
 我们把区间中的 **位置** 分成四类:在 $A,B$ 中同是区间最大值的位置、在 $A$ 中是区间最大值在 $B$ 中不是的位置、在 $B$ 中是区间最大值在 $A$ 中不是的位置、在 $A,B$ 中都不是区间最大值的位置。对这四类数分别维护 **答案** 和 **标记** 即可。举个例子,我们维护 $C_{1\sim 4},M_{1\sim 4},A_{max},B_{max}$ 分别表示当前区间中四类数的个数,四类数的答案的最大值, $A$ 序列的最大值、 $B$ 序列的最大值。然后合并信息该怎么合并就怎么合并了。
 
-复杂度仍是 $O(m\log_2^2 n)$ 。
+复杂度仍是 $O(m\log^2 n)$ 。
 
 ### 小结
 
@@ -425,4 +425,4 @@ $$
 1.  若干个加减操作标记的合并,没有接收过覆盖标记。
 2.  覆盖操作的标记,没有所谓的加减标记(加减标记转化为覆盖标记)
 
-于是我们把这个结点的 Pre 标记拆成 $(P_1,P_2)$ 。 $P_1$ 表示第一阶段的最大加减标记; $P_2$ 表示第二阶段的最大覆盖标记。利用相似的方法,我们可以对这个做标记下传和信息更新。时间复杂度是 $O(m\log_2 n)$ 的(这个问题并没有区间对 $x$ 取最值的操作哦~)
+于是我们把这个结点的 Pre 标记拆成 $(P_1,P_2)$ 。 $P_1$ 表示第一阶段的最大加减标记; $P_2$ 表示第二阶段的最大覆盖标记。利用相似的方法,我们可以对这个做标记下传和信息更新。时间复杂度是 $O(m\log n)$ 的(这个问题并没有区间对 $x$ 取最值的操作哦~)
index 2a46d6d..8d828a9 100644 (file)
@@ -62,7 +62,7 @@ void build(int s, int t, int p) {
 
 如果要查询的区间为 $[3,5]$ ,此时就不能直接获取区间的值,但是 $[3,5]$ 可以拆成 $[3,3]$ 和 $[4,5]$ ,可以通过合并这两个区间的答案来求得这个区间的答案。
 
-一般地,如果要查询的区间是 $[l,r]$ ,则可以将其拆成最多为 $O(\log_2 n)$ 个 **极大** 的区间,合并这些区间即可求出 $[l,r]$ 的答案。
+一般地,如果要查询的区间是 $[l,r]$ ,则可以将其拆成最多为 $O(\log n)$ 个 **极大** 的区间,合并这些区间即可求出 $[l,r]$ 的答案。
 
 此处给出 C++ 的代码实现,可参考注释理解:
 
index 1e5086b..8fb8bca 100644 (file)
@@ -1,6 +1,6 @@
 给你一个长度为 n 的序列 $\left\langle a_i\right\rangle_{i=1}^n$ ,再给你一个满足结合律的运算 $\circ$ (比如 $\gcd,\min,\max,+,\operatorname{and},\operatorname{or},\operatorname{xor}$ 均满足结合律),然后对于每一次区间询问 $[l,r]$ ,我们需要计算 $a_l\circ a_{l+1}\circ\dotsb\circ a_{r}$ 。
 
-Sqrt Tree 可以在 $O(n\log_2\log_2n)$ 的时间内预处理,并在 $O(1)$ 的时间内回答询问。
+Sqrt Tree 可以在 $O(n\log\log n)$ 的时间内预处理,并在 $O(1)$ 的时间内回答询问。
 
 ## 描述
 
@@ -42,13 +42,13 @@ $$
 
 ### 构建一棵树
 
-容易想到我们在每个块内递归地构造上述结构以支持块内的查询。对于大小为 $1$ 的块我们可以 $O(1)$ 地回答询问。这样我们就建出了一棵树,每一个结点代表序列的一个区间。叶子结点的区间长度为 $1$ 或 $2$ 。一个大小为 $k$ 的结点有 $O(\sqrt{k})$ 个子节点,于是整棵树的高度是 $O(\log_2\log_2n)$ 的,每一层的区间总长是 $O(n)$ 的,因此我们构建这棵树的复杂度是 $O(n\log_2\log_2n)$ 的。
+容易想到我们在每个块内递归地构造上述结构以支持块内的查询。对于大小为 $1$ 的块我们可以 $O(1)$ 地回答询问。这样我们就建出了一棵树,每一个结点代表序列的一个区间。叶子结点的区间长度为 $1$ 或 $2$ 。一个大小为 $k$ 的结点有 $O(\sqrt{k})$ 个子节点,于是整棵树的高度是 $O(\log\log n)$ 的,每一层的区间总长是 $O(n)$ 的,因此我们构建这棵树的复杂度是 $O(n\log\log n)$ 的。
 
-现在我们可以在 $O(\log_2\log_2n)$ 的时间内回答询问。对于询问 $[l,r]$ ,我们只需要快速找到一个区间长度最小的结点 $u$ 使得 $u$ 能包含 $[l,r]$ ,这样 $[l,r]$ 在 $u$ 的分块区间中一定是跨块的,就可以 $O(1)$ 地计算答案了。查询一次的总体复杂度是 $O(\log_2\log_2n)$ ,因为树高是 $O(\log_2\log_2n)$ 的。不过我们仍可以优化这个过程。
+现在我们可以在 $O(\log\log n)$ 的时间内回答询问。对于询问 $[l,r]$ ,我们只需要快速找到一个区间长度最小的结点 $u$ 使得 $u$ 能包含 $[l,r]$ ,这样 $[l,r]$ 在 $u$ 的分块区间中一定是跨块的,就可以 $O(1)$ 地计算答案了。查询一次的总体复杂度是 $O(\log\log n)$ ,因为树高是 $O(\log\log n)$ 的。不过我们仍可以优化这个过程。
 
 ### 优化询问复杂度
 
-容易想到二分高度,然后可以 $O(1)$ 判断是否合法。这样复杂度就变成了 $O(\log_2\log_2\log_2n)$ 。不过我们仍可以进一步加速这一过程。
+容易想到二分高度,然后可以 $O(1)$ 判断是否合法。这样复杂度就变成了 $O(\log\log\log n)$ 。不过我们仍可以进一步加速这一过程。
 
 我们假设
 
@@ -101,19 +101,18 @@ $$
 
 ### 更新一个区间
 
-Sqrt Tree 也支持区间覆盖操作 $\operatorname{Update}(l,r,x)$ ,即把区间 $[l,r]$ 的数全部变成 $x$ 。对此我们有两种实现方式,其中一种会花费 $O(\sqrt{n}\log_2\log_2n)$ 的复杂度更新信息, $O(1)$ 的时间查询;另一种则是 $O(\sqrt{n})$ 更新信息,但查询的时间会增加到 $O(\log_2\log_2n)$ 。
+Sqrt Tree 也支持区间覆盖操作 $\operatorname{Update}(l,r,x)$ ,即把区间 $[l,r]$ 的数全部变成 $x$ 。对此我们有两种实现方式,其中一种会花费 $O(\sqrt{n}\log\log n)$ 的复杂度更新信息, $O(1)$ 的时间查询;另一种则是 $O(\sqrt{n})$ 更新信息,但查询的时间会增加到 $O(\log\log n)$ 。
 
 我们可以像线段树一样在 Sqrt Tree 上打懒标记。但是在 Sqrt Tree 上有一点不同。因为下传一个结点的懒标记,复杂度可能达到 $O(\sqrt{n})$ ,因此我们不是在询问的时侯下传标记,而是看父节点是否有标记,如果有标记就把它下传。
 
 #### 第一种实现
 
-在第一种实现中,我们只会给第 $1$ 层的结点(结点区间长度为 $O(\sqrt{n})$ )打懒标记,在下传标记的时侯直接更新整个子树,复杂度为 $O(\sqrt{n}\log_2\log_2n)$ 。操作过程如下:
+在第一种实现中,我们只会给第 $1$ 层的结点(结点区间长度为 $O(\sqrt{n})$ )打懒标记,在下传标记的时侯直接更新整个子树,复杂度为 $O(\sqrt{n}\log\log n)$ 。操作过程如下:
 
 1.  考虑第 $1$ 层上的结点,对于那些被修改区间完全包含的结点,给他们打一个懒标记;
-2.  有两个块只有部分区间被覆盖,我们直接在 $O(\sqrt{n}\log_2\log_2n)$ 的时间内 **重建** 这两个块。如果它本身带有之前修改的懒标记,就在重建的时侯顺便下传标记;
+2.  有两个块只有部分区间被覆盖,我们直接在 $O(\sqrt{n}\log\log n)$ 的时间内 **重建** 这两个块。如果它本身带有之前修改的懒标记,就在重建的时侯顺便下传标记;
 3.  更新根结点的 $\left\langle P_i\right\rangle$ 和 $\left\langle S_i\right\rangle$ ,时间复杂度 $O(\sqrt{n})$ ;
-4.  重建 $index$ 树,时间复杂度 $O(\sqrt{n}\log_2\log_2n)$ 。
-
+4.  重建 $index$ 树,时间复杂度 $O(\sqrt{n}\log\log n)$ 。
 现在我们可以高效完成区间修改了。那么如何利用懒标记回答询问?操作如下:
 
 1.  如果我们的询问被包含在一个有懒标记的块内,可以利用懒标记计算答案;
@@ -123,7 +122,7 @@ Sqrt Tree 也支持区间覆盖操作 $\operatorname{Update}(l,r,x)$ ,即把
 
 #### 第二种实现
 
-在这种实现中,每一个结点都可以被打上懒标记。因此在处理一个询问的时侯,我们需要考虑祖先中的懒标记,那么查询的复杂度将变成 $O(\log_2\log_2n)$ 。不过更新信息的复杂度就会变得更快。操作如下:
+在这种实现中,每一个结点都可以被打上懒标记。因此在处理一个询问的时侯,我们需要考虑祖先中的懒标记,那么查询的复杂度将变成 $O(\log\log n)$ 。不过更新信息的复杂度就会变得更快。操作如下:
 
 1.  被修改区间完全包含的块,我们把懒标记添加到这些块上,复杂度 $O(\sqrt{n})$ ;
 2.  被修改区间部分覆盖的块,更新 $\left\langle P_i\right\rangle$ 和 $\left\langle S_i\right\rangle$ ,复杂度 $O(\sqrt{n})$ (因为只有两个被修改的块);
@@ -135,7 +134,7 @@ Sqrt Tree 也支持区间覆盖操作 $\operatorname{Update}(l,r,x)$ ,即把
 
 ## 代码实现
 
-下面的实现在 $O(n\log_2\log_2n)$ 的时间内建树,在 $O(1)$ 的时间内回答询问,在 $O(\sqrt{n})$ 的时间内单点修改。
+下面的实现在 $O(n\log\log n)$ 的时间内建树,在 $O(1)$ 的时间内回答询问,在 $O(\sqrt{n})$ 的时间内单点修改。
 
 ```cpp
 SqrtTreeItem op(const SqrtTreeItem &a, const SqrtTreeItem &b);
index ee81a76..2f3cf4c 100644 (file)
@@ -55,13 +55,13 @@ node *merge(node *u, node *v) {
 
 将一个有 $n$ 个节点的序列 $\{a_n\}$ 转化为一棵 treap。
 
-可以依次暴力插入这 $n$ 个节点,每次插入一个权值为 $v$ 的节点时,将整棵 treap 按照权值分裂成权值小于等于 $v$ 的和权值大于 $v$ 的两部分,然后新建一个权值为 $v$ 的节点,将两部分和新节点按从小到大的顺序依次合并,单次插入时间复杂度 $O(\log_2 n)$ ,总时间复杂度 $O(n\log_2 n)$ 。
+可以依次暴力插入这 $n$ 个节点,每次插入一个权值为 $v$ 的节点时,将整棵 treap 按照权值分裂成权值小于等于 $v$ 的和权值大于 $v$ 的两部分,然后新建一个权值为 $v$ 的节点,将两部分和新节点按从小到大的顺序依次合并,单次插入时间复杂度 $O(\log n)$ ,总时间复杂度 $O(n\log n)$ 。
 
 在某些题目内,可能会有多次插入一段有序序列的操作,这是就需要在 $O(n)$ 的时间复杂度内完成建树操作。
 
-方法一:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,并对每个节点钦定合适的优先值,使得新树满足堆的性质。这样能保证树高为 $O(\log_2 n)$ 。
+方法一:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,并对每个节点钦定合适的优先值,使得新树满足堆的性质。这样能保证树高为 $O(\log n)$ 。
 
-方法二:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,然后给每个节点一个随机优先级。这样能保证树高为 $O(\log_2 n)$ ,但不保证其满足堆的性质。这样也是正确的,因为无旋式 treap 的优先级是用来使 `merge` 操作更加随机一点,而不是用来保证树高的。
+方法二:在递归建树的过程中,每次选取当前区间的中点作为该区间的树根,然后给每个节点一个随机优先级。这样能保证树高为 $O(\log n)$ ,但不保证其满足堆的性质。这样也是正确的,因为无旋式 treap 的优先级是用来使 `merge` 操作更加随机一点,而不是用来保证树高的。
 
 方法三:观察到 treap 是笛卡尔树,利用笛卡尔树的 $O(n)$ 建树方法即可,用单调栈维护右脊柱即可。
 
index 9b58a98..24d9184 100644 (file)
@@ -10,7 +10,7 @@
 
 对于一个子树中简单路径的计算,我们选择一个分治中心 $rt$ ,计算经过该节点的子树中路径的信息,然后对于其每个儿子结点,将删去 $rt$ 后该点所在连通块作为一个子树,递归计算。选择的分治中心点可以构成一个树形结构,称为 **点分树** 。我们发现,计算点分树中同一层的结点所代表的连通块(即以该结点为分治中心的连通块)的大小总和是 $O(n)$ 的。这意味着,点分治的时间复杂度是与点分树的深度相关的,若点分树的深度为 $h$ ,则点分治的复杂度为 $O(nh)$ 。
 
-可以证明,当我们每次选择连通块的重心作为分治中心的时候,点分树的深度最小,为 $O(\log_2 n)$ 的。这样,我们就可以在 $O(n\log_2 n)$ 的时间复杂度内统计树上 $O(n^2)$ 条路径的信息了。
+可以证明,当我们每次选择连通块的重心作为分治中心的时候,点分树的深度最小,为 $O(\log n)$ 的。这样,我们就可以在 $O(n\log n)$ 的时间复杂度内统计树上 $O(n^2)$ 条路径的信息了。
 
 由于树的形态在动态点分治的过程中不会改变,所以点分树的形态在动态点分治的过程中也不会改变。
 
@@ -56,9 +56,9 @@ int main() {
 
 ### 实现修改
 
-在查询和修改的时候,我们在点分树上暴力跳父亲修改。由于点分树的深度最多是 $O(\log_2 n)$ 的,所以这样做复杂度能得到保证。
+在查询和修改的时候,我们在点分树上暴力跳父亲修改。由于点分树的深度最多是 $O(\log n)$ 的,所以这样做复杂度能得到保证。
 
-在动态点分治的过程中,需要一个结点到其点分树上的祖先的距离等其他信息,由于一个点最多有 $O(\log_2 n)$ 个祖先,我们可以在计算点分树时额外计算深度 $dep[x]$ 或使用 LCA,预处理出这些距离或实现实时查询。 **注意** :一个结点到其点分树上的祖先的距离不一定递增,不能累加!
+在动态点分治的过程中,需要一个结点到其点分树上的祖先的距离等其他信息,由于一个点最多有 $O(\log n)$ 个祖先,我们可以在计算点分树时额外计算深度 $dep[x]$ 或使用 LCA,预处理出这些距离或实现实时查询。 **注意** :一个结点到其点分树上的祖先的距离不一定递增,不能累加!
 
 在动态点分治的过程中,一个结点在其点分树上的祖先结点的信息中可能会被重复计算,这是我们需要消去重复部分的影响。一般的方法是对于一个连通块用两种方式记录:一个是其到分治中心的距离信息,另一个是其到点分树上分治中心父亲的距离信息。这一部分内容将在例题中得到展现。
 
@@ -73,7 +73,7 @@ int main() {
 
 求出点分树,对于每个结点 $x$ 维护两个 **可删堆** 。 $dist[x]$ 存储结点 $x$ 代表的连通块中的所有黑点到 $x$ 的距离信息, $ch[x]$ 表示结点 $x$ 在点分树上的所有儿子和它自己中的黑点到 $x$ 的距离信息,由于本题贪心的求答案方法,且两个来自于同一子树的路径不能成为一条完成的路径,我们只在这个堆中插入其自己的值和其每个子树中的最大值。我们发现, $ch[x]$ 中最大的两个值(如果没有两个就是所有值)的和就是分治时分支中心为 $x$ 时经过结点 $x$ 的最长黑端点路径。我们可以用可删堆 $ans$ 存储所有结点的答案,这个堆中的最大值就是我们所求的答案。
 
-我们可以根据上面的定义维护 $dist[x],ch[x],ans$ 这些可删堆。当 $dist[x]$ 中的值发生变化时,我们也可以在 $O(\log_2 n)$ 的时间复杂度内维护 $ch[x],ans$ 。
+我们可以根据上面的定义维护 $dist[x],ch[x],ans$ 这些可删堆。当 $dist[x]$ 中的值发生变化时,我们也可以在 $O(\log n)$ 的时间复杂度内维护 $ch[x],ans$ 。
 
 现在我们来看一下,当我们反转一个点的颜色时, $dist[x]$ 值会发生怎样的改变。当结点原来是黑色时,我们要进行的是删除操作;当结点原来是白色时,我们要进行的是插入操作。
 
index a6272ef..fa1406b 100644 (file)
@@ -14,7 +14,7 @@ A\*算法定义了一个对当前状态 $x$ 的估价函数 $f(x)=g(x)+h(x)$ ,
 
 优化:由于只需要求出从初始结点到目标结点的第 $k$ 短路,所以已经取出的状态到达一个结点的次数大于 $k$ 次时,可以不扩展其子状态。因为之前 $k$ 次已经形成了 $k$ 条合法路径,当前状态不会影响到最后的答案。
 
-当图的形态是一个 $n$ 元环的时候,该算法最坏是 $O(nk\log_2 n)$ 的。但是这种算法可以在相同的复杂度内求出从起始点 $s$ 到每个结点的前 $k$ 短路。
+当图的形态是一个 $n$ 元环的时候,该算法最坏是 $O(nk\log n)$ 的。但是这种算法可以在相同的复杂度内求出从起始点 $s$ 到每个结点的前 $k$ 短路。
 
 ### 参考实现
 
@@ -141,13 +141,13 @@ int main() {
 
 用这种方法,每次生成新的边集只会扩展出最多三个结点,小根堆中的结点总数是 $O(n+k)$ 。
 
-所以此算法的瓶颈在合并一个结点与其在 $T$ 上的祖先的信息,如果使用朴素的二叉堆,时间复杂度为 $O(nm\log_2 m)$ ,空间复杂度为 $O(nm)$ ;如果使用可并堆,每次仍然需要复制堆中的全部结点,时间复杂度同样无法承受。
+所以此算法的瓶颈在合并一个结点与其在 $T$ 上的祖先的信息,如果使用朴素的二叉堆,时间复杂度为 $O(nm\log m)$ ,空间复杂度为 $O(nm)$ ;如果使用可并堆,每次仍然需要复制堆中的全部结点,时间复杂度同样无法承受。
 
 ### 可持久化可并堆优化
 
  **在阅读本内容前,请先了解 [可持久化可并堆](/ds/persistent-heap) 的相关知识。** 
 
-使用可持久化可并堆优化合并一个结点与其在 $T$ 上的祖先的信息,每次将一个结点与其在 $T$ 上的父亲合并,时间复杂度为 $O(n\log_2 m)$ ,空间复杂度为 $O((n+k)\log_2 m)$ 。这样在求出一个结点对应的堆时,无需复制结点且之后其父亲结点对应的堆仍然可以正常访问。
+使用可持久化可并堆优化合并一个结点与其在 $T$ 上的祖先的信息,每次将一个结点与其在 $T$ 上的父亲合并,时间复杂度为 $O(n\log m)$ ,空间复杂度为 $O((n+k)\log m)$ 。这样在求出一个结点对应的堆时,无需复制结点且之后其父亲结点对应的堆仍然可以正常访问。
 
 ### 参考实现
 
index c388cfa..8e84642 100644 (file)
@@ -18,7 +18,7 @@ Heinz Prufer 于 1918 年发明这个序列来证明凯莱定理。
 
 Prufer 是这样建立的:每次选择一个编号最小的叶结点并删掉它,然后在序列中记录下它连接到的那个结点。重复 $n-2$ 次后就只剩下两个结点,算法结束。
 
-显然使用堆可以做到 $O(n\log_2n)$ 的复杂度
+显然使用堆可以做到 $O(n\log n)$ 的复杂度
 
 ```cpp
 // 代码摘自原文,结点是从 0 标号的
@@ -125,7 +125,7 @@ vector<int> pruefer_code() {
 
 重建树的方法是类似的。根据 Prufer 序列的性质,我们可以得到原树上每个点的度数。然后你也可以得到度数最小的叶结点编号,而这个结点一定与 Prufer 序列的第一个数连接。然后我们同时删掉这两个结点的度数。
 
-讲到这里也许你已经知道该怎么做了。每次我们选择一个度数为 $1$ 的最小的结点编号,与当前枚举到的 Prufer 序列的点连接,然后同时减掉两个点的度。到最后我们剩下两个度数为 $1$ 的点,其中一个是结点 $n$ 。就把它们建立连接。使用堆维护这个过程,在减度数的过程中如果发现度数减到 $1$ 就把这个结点添加到堆中,这样做的复杂度是 $O(n\log_2n)$ 的。
+讲到这里也许你已经知道该怎么做了。每次我们选择一个度数为 $1$ 的最小的结点编号,与当前枚举到的 Prufer 序列的点连接,然后同时减掉两个点的度。到最后我们剩下两个度数为 $1$ 的点,其中一个是结点 $n$ 。就把它们建立连接。使用堆维护这个过程,在减度数的过程中如果发现度数减到 $1$ 就把这个结点添加到堆中,这样做的复杂度是 $O(n\log n)$ 的。
 
 ```cpp
 // 原文摘代码
index 201b1e3..e9f23e5 100644 (file)
@@ -15,7 +15,7 @@
 
 点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归 $h$ 层,则总时间复杂度为 $O(h\times n)$ 。
 
-若我们 **每次选择子树的重心作为根节点** ,可以保证递归层数最少,时间复杂度为 $O(n\log_2 n)$ 。
+若我们 **每次选择子树的重心作为根节点** ,可以保证递归层数最少,时间复杂度为 $O(n\log n)$ 。
 
 请注意在重新选择根节点之后一定要重新计算子树的大小,否则一点看似微小的改动就可能会使时间复杂度错误或正确性难以保证。
 
index b447d28..9dbfc63 100644 (file)
@@ -252,7 +252,7 @@ $$
 
 > 求欧拉函数 $\varphi(n)$ . 其中 $\varphi(n)=|\{1\leq x\leq n|\gcd(x,n)=1\}|$ .
 
-直接计算是 $O(n\log_2n)$ 的,用线性筛是 $O(n)$ 的,杜教筛是 $O(n^{\frac{2}{3}})$ 的(话说一道数论入门题用容斥做为什么还要扯到杜教筛上),接下来考虑用容斥推出欧拉函数的公式
+直接计算是 $O(n\log n)$ 的,用线性筛是 $O(n)$ 的,杜教筛是 $O(n^{\frac{2}{3}})$ 的(话说一道数论入门题用容斥做为什么还要扯到杜教筛上),接下来考虑用容斥推出欧拉函数的公式
 
 判断两个数是否互质,首先分解质因数
 
index fecbae3..aa188b4 100644 (file)
@@ -349,7 +349,7 @@ $$
 C_k = \underbrace{G \cdot G \cdots G}_{k \text{ 次}} = G^k
 $$
 
-要计算这个矩阵幂,我们可以使用快速幂(二进制取幂)的思想,在 $O(n^3 \log_2 k)$ 的复杂度内计算结果。
+要计算这个矩阵幂,我们可以使用快速幂(二进制取幂)的思想,在 $O(n^3 \log k)$ 的复杂度内计算结果。
 
 ### 定长最短路
 
@@ -382,7 +382,7 @@ $$
 L_k = \underbrace{G \odot \ldots \odot G}_{k\text{ 次}} = G^{\odot k}
 $$
 
-我们仍然可以用矩阵快速幂的方法计算上式,因为它显然是具有结合律的。时间复杂度 $O(n^3 \log_2 k)$ 。
+我们仍然可以用矩阵快速幂的方法计算上式,因为它显然是具有结合律的。时间复杂度 $O(n^3 \log k)$ 。
 
 ### 限长路径计数/最短路
 
index 3980c67..d6c3893 100644 (file)
@@ -199,14 +199,14 @@ $$
     \end{bmatrix}
     $$
 
-现在,每一种操作都被表示为了一个矩阵,变换序列可以用矩阵的乘积来表示,而一个 Loop 操作相当于取一个矩阵的 k 次幂。这样可以用 $O(m \log_2{k})$ 计算出整个变换序列最终形成的矩阵。最后将它应用到 $n$ 个点上,总复杂度 $O(n + m \log_2k)$ 。
+现在,每一种操作都被表示为了一个矩阵,变换序列可以用矩阵的乘积来表示,而一个 Loop 操作相当于取一个矩阵的 k 次幂。这样可以用 $O(m \log k)$ 计算出整个变换序列最终形成的矩阵。最后将它应用到 $n$ 个点上,总复杂度 $O(n + m \log k)$ 。
 
 ### 定长路径计数
 
 ???+note "问题描述"
     给一个有向图(边权为 1),求任意两点 $u,v$ 间从 $u$ 到 $v$ ,长度为 $k$ 的路径的条数。
 
-我们把该图的邻接矩阵 M 取 k 次幂,那么 $M_{i,j}$ 就表示从 $i$ 到 $j$ 长度为 $k$ 的路径的数目。该算法的复杂度是 $O(n^3 \log_2 k)$ 。有关该算法的细节请参见 [矩阵](./matrix.md) 页面。
+我们把该图的邻接矩阵 M 取 k 次幂,那么 $M_{i,j}$ 就表示从 $i$ 到 $j$ 长度为 $k$ 的路径的数目。该算法的复杂度是 $O(n^3 \log k)$ 。有关该算法的细节请参见 [矩阵](./matrix.md) 页面。
 
 ### 模意义下大整数乘法
 
index 6ab8502..7dd8f74 100644 (file)
@@ -4,8 +4,6 @@ author: linehk
 
 一般来说,复杂度是一个关于数据规模的函数。对于某些算法来说,相同数据规模的不同数据依然会造成算法的运行时间/空间的不同,因此我们通常使用算法的最坏时间复杂度,记为 $T(n)$ 。对于一些特殊的情况,我们可能会关心它的平均情况复杂度(特别是对于随机算法 (randomized algorithm)),这个时候我们通过使用随机分析 (probabilistic analysis) 来得到期望的复杂度。
 
-另外请读者们注意(尤其是刚入门的 OIer),由于在算法竞赛中,算法复杂度带对数时,对数的底通常为 $2$ ,因此常将对数的底数省略不写(即将 $\log_2{n}$ 简写为 $\log n$ )。当底数不为 $2$ 时一般会标明,所以当读者看到复杂度中出现 $\log n$ 的时侯就要知道它的底数是 $2$ 。
-
 ## 渐进符号
 
 我们通常使用渐进符号来描述一个算法的复杂度。
@@ -30,7 +28,7 @@ author: linehk
 
 -    $f_1(n) + f_2(n) = O(\max(f_1(n), f_2(n)))$ 
 -    $f_1(n) \times f_2(n) = O(f_1(n) \times f_2(n))$ 
--   任何对数函数无论底数为何,都具有相同的增长率。 $\forall a \neq 1, \log_a{n} = O(\log_2 n)$ 
+-    $\forall a \neq 1, \log_a{n} = O(\log_2 n)$ 。由换底公式可以得知,任何对数函数无论底数为何,都具有相同的增长率,因此渐进时间复杂度中对数的底数一般省略不写。 
 
 ## 主定理 (Master Theorem)
 
@@ -52,7 +50,7 @@ $$
 算法往往是会对内存中的数据进行修改的,而同一个算法的多次执行,就会通过对数据的修改而互相影响。
 
 例如快速排序中的“按大小分类”操作,单次执行的最坏时间复杂度,看似是 $O(n)$ 的。
-但是由于快排的分治过程,先前的“分类”操作每次都减小了数组长度,所以实际的总复杂度 $O(n \log_2 n)$ ,分摊在每一次“分类”操作上,是 $O(\log_2 n)$ 。
+但是由于快排的分治过程,先前的“分类”操作每次都减小了数组长度,所以实际的总复杂度 $O(n \log n)$ ,分摊在每一次“分类”操作上,是 $O(\log n)$ 。
 
 多次操作的总复杂度除以操作次数,就是这种操作的 **均摊复杂度** 。
 
index 33e560f..e8fbab6 100644 (file)
@@ -47,9 +47,9 @@
 
 可以对于每个询问进行一次二分;但是,也可以把所有的询问放在一起二分。
 
-先考虑二分的本质:假设要猜一个 $[l,r]$ 之间的数,猜测之后会知道是猜大了,猜小了还是刚好。当然可以从 $l$ 枚举到 $r$ ,但更优秀的方法是二分:猜测答案是 $m = \lfloor\frac{l + r}{2}\rfloor$ ,然后去验证 $m$ 的正确性,再调整边界。这样做每次询问的复杂度为 $O(n\log_2 n)$ ,若询问次数为 $q$ ,则时间复杂度为 $O(qn\log_2 n)$ 。
+先考虑二分的本质:假设要猜一个 $[l,r]$ 之间的数,猜测之后会知道是猜大了,猜小了还是刚好。当然可以从 $l$ 枚举到 $r$ ,但更优秀的方法是二分:猜测答案是 $m = \lfloor\frac{l + r}{2}\rfloor$ ,然后去验证 $m$ 的正确性,再调整边界。这样做每次询问的复杂度为 $O(n\log n)$ ,若询问次数为 $q$ ,则时间复杂度为 $O(qn\log n)$ 。
 
-回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 $mid$ ,然后去依次验证每个询问的答案应该是小于等于 $mid$ 的还是大于 $mid$ 的,并将询问分为两个部分(不大于/大于),对于每个部分继续二分。注意:如果一个询问的答案是大于 $mid$ 的,则在将其划至右侧前需更新它的 $k$ ,即,如果当前数列中小于等于 $mid$ 的数有 $t$ 个,则将询问划分后实际是在右区间询问第 $k - t$ 小数。如果一个部分的 $l = r$ 了,则结束这个部分的二分。利用线段树的相关知识,我们每次将整个答案可能在的区间 $[1,maxans]$ 划分成了若干个部分,这样的划分共进行了 $O(\log_2 maxans)$ 次,一次划分会将整个操作序列操作一次。若对整个序列进行操作,并支持对应的查询的时间复杂度为 $O(T)$ ,则整体二分的时间复杂度为 $O(T\log_2 n)$ 。
+回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 $mid$ ,然后去依次验证每个询问的答案应该是小于等于 $mid$ 的还是大于 $mid$ 的,并将询问分为两个部分(不大于/大于),对于每个部分继续二分。注意:如果一个询问的答案是大于 $mid$ 的,则在将其划至右侧前需更新它的 $k$ ,即,如果当前数列中小于等于 $mid$ 的数有 $t$ 个,则将询问划分后实际是在右区间询问第 $k - t$ 小数。如果一个部分的 $l = r$ 了,则结束这个部分的二分。利用线段树的相关知识,我们每次将整个答案可能在的区间 $[1,maxans]$ 划分成了若干个部分,这样的划分共进行了 $O(\log maxans)$ 次,一次划分会将整个操作序列操作一次。若对整个序列进行操作,并支持对应的查询的时间复杂度为 $O(T)$ ,则整体二分的时间复杂度为 $O(T\log n)$ 。
 
 试试完成以下代码: