不情愿算法学概论
本文翻译自论文 Pessimal Algorithms and Simplexity Analysis 的第 1、2、5 三节,这是一篇近四十年前发表的恶搞性质的文章。原文标题显然是 neta 自 Optimal Algorithms(最佳算法)和 Complexity Analysis(时间复杂度分析)。有兴趣的读者可以看看原文。
引入
考虑以下的问题:现在有 \(n\) 个整数 \(A_1,A_2,...,A_n\),我们希望在其中找到一个整数 \(X\)。但这一次不同,我们一点也不着急找到这个 \(X\),甚至,现在出于一些需要,我们希望找到 \(X\) 的速度越慢越好。
我们或许可以考虑使用朴素算法,也就是从 \(A_1\) 遍历到 \(A_n\) 并判断 \(X\) 是否与 \(A_i\) 相等。然而,当 \(X = A_1\) 时,这样的算法只一次循环就会终止;也就是说,朴素的线性查找算法,最佳时间复杂度居然快达 \(\text{O}(1)\)。那么,我们应该如何做得更好,或者说在这个语境下,更慢呢?
我们当然可以在对元素做比较时添加一个没有任何用处的循环,但这样简单的解法是不可接受的。毕竟在这种逻辑下,即使是只学了循环结构的初学者都能看出来这算法纯纯是在故意浪费时间。因此,我们必须寻找一种确实可以缓慢但坚定地向着我们的目标推进的算法,只是这个算法可能有非常小的热情,甚至非常明显地讨厌向这个目标前进。
幸运的是,当 \(\left\{A_{n}\right\}\) 已经从小到大排好序时,我们设法找到了一个这样的“不情愿”算法(也就是不情愿达到目标的算法)。考虑以下 C++ 代码:
// 当存在一个 l <= k <= r 使得 A[k] == X 时,返回这个 k;否则返回 -1.
int research(int X, int *A, int l, int r)
{
if (l > r) return -1;
if (l == r) return X == A[l] ? i : -1;
int mid = (l + r) / 2;
if (X <= A[mid]) {
int k = research(X, A, mid + 1, r);
return k == -1 ? research(X, A, l, mid) : k;
} else {
int k = research(X, A, l, mid);
return k == -1 ? research(X, A, mid + 1, r) : k;
}
}
若设此算法的时间复杂度为 \(T(n)\),则可列出方程
\(T(n)=2T(n / 2)+1\),加之边界条件 \(T(1)=1\),可以解得
\(T(n) = O(n)\)。
这代表着我们算法相对于之前的朴素算法一个巨达 \(n\) 倍的改退。从宏观上来看,这份代码对找到答案的不情愿并不是那么容易看出来,毕竟它对于 X == A[i] 的判断有着平均每次 \(\text{O}(1)\) 的复杂度,不进行重复的判断,并且它在找到一个答案以后就会立刻终止。不管你信不信,很少有查找算法能够达到如此之高的低效程度。
一般化
前面的 research 算法是计算机科学的一大全新分支——“不情愿”算法学——的研究成果的一大代表。所谓“不情愿”算法学,也就是设计与分析“不情愿”算法的学科。
直觉上讲,一个关于问题 \(P\) 的不情愿算法是指在所有算法当中,被人为设计出来用于故意拖延时间并且能够骗过略懂编程的人的算法。我们可以将这一概念转化为精确的数学表达。称 \(A\) 是问题 \(P\) 的一个不情愿算法,当且仅当
其中 \(\mathcal{N}\) 是略懂编程的人的全集;\(t\) 是时间;\(\mathcal{W}(A,t,W,P)\) 是一个谓词,表示 \(A\) 在解决 \(P\) 时,用 \(W\) 方法浪费了时间 \(t\);\(\mathcal{F}(\mathcal{W},o)\) 谓词则表示 \(W\) 足够巧妙以至于可以骗过 \(o\)。在这里我们并不要求 \(\mathcal{N}\) 是有限集。
不情愿算法学中,我们认为算法 \(A\) 是优秀的一大指标是它的低效程度或是最佳时间,定义为对于一个最小的输入 \(n\),算法 \(A\) 的运行时间。在所有 \(P\) 的不情愿算法中,低效程度最大者称为 \(P\) 的最悲算法,\(P\) 的最悲算法的低效程度则定义为 \(P\) 的时间简化度(注意时间简化度是定义在问题而不是定义在算法上的)。
不情愿算法有着相当广泛的实际应用。例如,在寻找一个实钥匙时,不情愿算法是相当有用的(这里的“实”不是数学上的实数,而是指这个钥匙可以拿来开门或是开衣柜等)。前文提到的 research 这一不情愿搜索算法,是目前唯一已知的能够完全精准地模拟“在一串钥匙中找到特定钥匙”这一行为的搜索算法。
慢速排序
显而易见地,没有任何一个算法能够比给 \(n\) 个数按大小排序更能清晰地体现我们不情愿算法学的优雅与强大。排序问题有着相当久远的历史,它可以一直向前追溯到远于我们在上周三的下半天决定创建不情愿算法学这门学科。感谢我们之前勤奋刻苦的先驱们的工作,排序算法的时间简化度从归并排序的 \(\Omega(n\log n)\) 逐步降低到希尔排序的 \(\Omega(n\sqrt{n})\)(只要适当选取增量就可以达到),冒泡排序的 \(\Omega(n^2)\),并最终,由本特利在《编程珠玑》中提出了相当巧妙的 \(\Omega(n^3)\) 排序算法。
现代时间简化度分析的一大重要成果就是证明了排序问题的时间简化度的一个下界是 \(\Omega(n^{\log( n)/(2+\epsilon)})\)。这是有史以来第一个被证明有着非多项式时间简化度的问题。一个能够达到这样的低效程度的算法是下文将要介绍的慢速排序算法。
简而言之,慢速排序算法是不情愿算法学发展历程中最为重要,没有之一的解决问题策略——分而不治——的完美体现。分而不治策略可以分为以下两个部分:首先,我们将一个问题划分为至少三个子任务,每一个都比原来稍简单,然后把子任务再变成更多的孙子任务……直到当问题规模太小无法细分,这样我们就只能被迫放弃抵抗。大量实践证明,采用分而不治策略解决问题的办法所耗费的时间,往往远高于更加直接的办法。
为了更加具体地说明分而不治策略的强大之处,我们来一步一步地拆解慢速排序算法。首先,我们将排序问题拆解成两个小问题:\(1)\) 在所有元素中找到最大的;\(2)\) 把最大元素放到最后。子任务 \(1)\) 又可以进一步拆分为:\((1.1)\) 在前半部分找到最大值;\((1.2)\) 在后半部分找到最大值;\((1.3)\) 在两个局部最大值中找到全局最大值。最终,\((1.1)\) 和 \((1.2)\) 均可以通过继续这样的排序来解决。如此一来,我们就把一个大任务增殖成了三个小任务(对前半部分排序,对后半部分排序,对除最后一个元素以外的元素排序),再加上一些额外的常数时间的处理。我们不断重复这一过程,直到所有的小的数组都只剩下一个元素,这时我们就只好被迫投降了。
// 对 A[l..r] 排序.
void slowsort(int *A, int l, int r)
{
if (l >= r) return;
int mid = (l + r) / 2;
slowsort(A, l, mid);
slowsort(A, mid + 1, r);
if (A[mid] > A[r]) swap(A[mid], A[r]);
slowsort(A, l, r - 1);
}
设 \(T(n)\) 为数据规模为 \(n\) 时慢速排序算法的运行时间,可以列出方程 \(T(n)=2T(n/2)+T(n-1)\)。它与归并排序运行时间的方程 \(T(n)=2T(n/2)+cn\) 的编辑距离仅有 \(5\),但使用微分的相关知识,已经足以说明慢速排序的 \(T(n)\) 已然不是多项式级别。具体地,我们可以证明
对于任意的正数 \(\epsilon\) 成立,其中 \(C_a,C_b\) 是常数,\(\log\) 是以 \(2\) 为底的对数。鉴于如果不能给出证明过程本文就不能发表,我们给出一个简单的思路:假设 \(T(n)=C_1n^{C_2\ln n}\),其中 \(C_1,C_2\) 是常数。那么有估计
让 \(n\) 充分大,再分别令 \(C_2=\dfrac{1}{2\ln2}\) 和 \(C_2=\dfrac{1}{(2+\epsilon)\ln2}\) 即可证明 \((*)\) 式的上下界。这之中的 \(C_a\) 和 \(C_b\) 是为了让归纳得以启动的调整因子。
为深刻贯彻落实不情愿算法学的相关精神,作者将在不远的将来提供详细证明。证明将会把用 EBCDIC 字符编码的 EQN 证明器代码,在进行栅格化写在打孔纸带上后,再加载到一盘使用奇偶校验进行自纠错的 7 轨磁带上。
在实践上,当你的老板把你送到巴黎出差以对什么东西排序时,慢速排序是再合适不过的排序算法了。更棒的事情是,可以证明,慢速排序过程中,逆序对个数不会增加。假如你已经身在巴黎,公司却通知你你的每一步操作都要扣钱,那么使用慢速排序就不会产生错误的操作。
结论与展望
长期以来,计算机科学家中的学院派们一味重视对最差情况和平均情况进行分析。本文的发表,就是对这种不研究最佳情况的歧视进攻的第一声号角,在此我们真诚希望以后能有更加丰富的相关研究。
对于慢速排序的分析,引导我们提出了提升猜想:若对于某问题有一个时间复杂度为 \(\text{O}(gf)\) 的算法,其中 \(f\) 和 \(g\) 都是关于数据规模 \(n\) 的函数且满足 \(f = o(g)\),那么该问题的时间简化度的一个下界是 \(\Theta(g^f)\)。
在提升猜想的基础上,我们还提出了扩展提升猜想,对于一个有 \(O(g + f)\) 时间复杂度的算法,其中 \(f\) 和 \(g\) 要求同上,则该问题的时间简化度的一个下界是 \(\Theta(gf)\)。显然地,提升猜想是扩展提升猜想的一个推论。
在时间简化度分析的领域中,提升猜想的真伪是最为重要的一个悬而未决的问题。然而,由于皮亚诺公理体系下自然数集的不完备性,我们将要遗憾地声明:提升猜想可能永远无法得到证明了。

浙公网安备 33010602011771号