探底排序的时间复杂度
很久很久之前看到过这么一个问题:炉石里一套牌共有 30 张,问至少要探底多少次才能保证可以将牌库排序成想要的顺序?这里给不了解背景的朋友介绍一下,所谓 探底,就是选择牌库底三张牌中的一张将其至于牌库顶。
我们来抽象一下这个题:对于一个大小为 的排列 ,一次操作可以把前三个数中的一个塞到最后。既然这是个排序算法,我们不妨将其称作 探底排序 好了…… 那么现在问题就是问探底排序的最坏情况下需要多少次操作。我们先研究一个简化版,每次只能把前两个元素中的一个塞到最后。
~~作为一名 TCS 大师,~~我对这个问题挺有兴趣的。但是显然,求出精确解看起来非常难(而且我怀疑甚至不存在通项),于是就退而求其次,分析一下其时间复杂度好了。
我们先来给出一个 的算法,顺便证明其一定有解。idea 非常简单,我们就模仿选择排序,每次选出剩下的数中最小的一个。具体说来,算法分为 轮。第 轮过后,我们保证整个序列最后面 个元素一定依次是 1 到 ,这样 轮之后整个数组就有序了。在第 轮中,前 次,我们比较排列前两个数,并且将较大的那个数塞到最后去,这样 次之后,序列中第一个数一定是 ,且接下来 个数依次为 ,于是我们再把这 个数塞到最后,最后把 塞到最后即可。伪代码如下:
for k from 1 to n:
for _ from 1 to n - k: # 选 P[1] ... P[n - k + 1] 中最小的数
if P[1] > P[2]:
rotate(1)
else:
rotate(2)
for _ from 1 to k - 1: # 此时 P[1] 一定是 k,将接下来的 1..k-1 塞到最后
rotate(2)
rotate(1) # 把 k 塞到最后去
复杂度分析就很简单了,显然是 ,更确切一点,是刚好 。
既然 upper bound 有了,我们再来整一个 lower bound 吧。直觉上应该也是 ,而且感觉把整个序列倒过来就需要这么多。不妨让我们来打个表试试:
n = 1, worst = 0, reverse = 0
n = 2, worst = 1, reverse = 1
n = 3, worst = 2, reverse = 2
n = 4, worst = 5, reverse = 4
n = 5, worst = 8, reverse = 6
n = 6, worst = 12, reverse = 11
n = 7, worst = 17, reverse = 14
n = 8, worst = 23, reverse = 22
n = 9, worst = 30, reverse = 26
n = 10, worst = 38, reverse = 37
我直接去 OEIS 上搜了一下 worst 这个序列,没找到,但是看着像是一个 的序列,而且我猜测奇数项和偶数项各自组成一个二次多项式。而且 为偶数的情况下,序列翻转几乎就是最坏情况了。接下来我们证明其 lower bound 为 。
我们首先给出一个核心观察:
对于任何一个长度为 的操作序列 , 的前 个数对应的集合和 的后 个数对应的集合至多只有一个数不同。
这个观察很好理解: 的后 个元素只能全部来自于 的前 个元素。这个观察带来的一个推论是,进行恰好 次操作后, 的前 个数和 的前 个数至多只有两个数不同(集合意义上)。
有了这个推论,我们就可以开始证明 lower bound 了。对于排列 ,假设 为最优操作的次数, 为最优操作序列。我们把 分成若干段,第一段为 ,长度为 ,之后每段长度为 。由于后面 次操作将 进行排序,我们马上可以得到一个 lower bound:
其中 表示序列 前一半中有多少个元素排序后不在前一半了。接下来,我们证明存在一个 ,使得对于任何序列长度不超过 的 ,都有 ,这样就能够整出一个 的 lower bound。
令 ,我们把 均匀分成 4 块 ,其中 ,,,,并令 为一个 的排列。为了得到了 的一个 lower bound,我们对 分类:
- 如果 ,那么 前一半中至少有 个 ,故 ;
- 如果 ,那么 后一半中至少有 个 ,故 ;
- 如果 ,那么 前一半中至少有 个 ,故 ;
- 如果 ,那么 后一半中至少有 个 ,故 。
这样,我们就代入之前的结果可得:
到这里,我们就得到了一个 tight 的 bound,。但是还差那么一点点:打表给出的 的系数为 ,lower bound 给出的系数是 ,但是算法给出的 的系数为 1。我们能否把上下界的常数都改进到 呢?