TL;DR:可以修改快速排序以获得以下保证:在(预期)时间 O(n log n) 内,我们可以执行以下操作之一,具体取决于翻转的比较。
- 对数组进行完美排序。
- 对数组进行完美排序,但数组中某处的相邻项对被交换。
- 对数组进行完美排序,只是数组中连续三个可以识别的项被置换了。
这保证了最大位移为 2,这在理论上是可能的。
我考虑了这个问题几个小时,我所做的一切都连接到tournaments。
我想首先尝试重新构建问题,如下所示。如果您有一组 n 个项目,并且您知道它们之间比较的“真实”结果,则可以将该结果表示为一个有向图,每个项目一个节点,边指示一个项目何时比另一个少。这种类型的有向图称为“锦标赛”,因为您可以将其视为对循环锦标赛的结果进行编码,其中每个玩家与其他玩家比赛。
在诚实比较器的情况下,我们的锦标赛将是非循环的,特别是它具有以下关键属性:每个出度 0、1、2、...、n - 1 恰好有一个节点。这里的想法是最小元素的出度为 n - 1(它比其他所有元素都小),而最大的元素的出度为 0(它比其他所有元素都大)。事实上,有一个定理,当且仅当锦标赛中的每个节点具有不同的出度时,锦标赛才是非循环的。另一个有用的事实:在非循环锦标赛中,当且仅当 outdeg(U) > outdeg(V) 时,从 U 到 V 存在边。
在“不诚实的比较器”的情况下,我们基本上从非循环锦标赛开始,然后翻转单边。你的问题是关于根据这个比较器进行近似排序,但我想退后一步,问一个不同的问题,我认为可以用来更准确地回答你的问题。在什么情况下,你能确定哪条边被翻转了?如果我们能做到这一点,那么我们可以做得比近似排序更好——我们可以“取消翻转”边缘并完美排序。另一方面,在哪些情况下你不能确定哪条边被翻转了,当这种情况发生时,我们离排序还有多远?这对应于必须进行近似排序,因为我们无法恢复原始排序。
这是一个有用的事实:
定理:从非循环锦标赛开始并翻转单边。那么,当且仅当翻转边的两个端点的出度最初相差至少三倍时,才可能确定翻转了哪条边。
为了证明这一点,我们将展示两个隐含方向。
首先,假设我们翻转两个节点 X 和 Y 之间的边,它们的出度相差 1。当我们完成后,我们剩下一个锦标赛,其中所有节点都有不同的出度(所有其他节点的出度不变,如果我们翻转边缘(X,Y),那么 X 和 Y 交换出度,因为一个上升一个和一个下降一个)。我们现在剩下另一个非循环锦标赛。特别是,我们无法判断我们翻转了哪条边,因为我们也可以翻转出度相差 1 的任何一对节点之间的任何边。
接下来,假设我们在节点 X 和 Y 之间翻转一条边,其中 outdeg(X) = k+1 和 outdeg(Y) = k-1。我们现在有 outdeg(X) = k = outdeg(Y),并且在其他地方开始时肯定有某个节点 Z 也有 outdegree k。所以此时,我们有三个出度为 k 的节点(即 X、Y 和 Z),我们知道我们必须翻转它们之间的三个边之一。但我们无法判断它是哪一个。具体来说,翻转 XY 边缘、XZ 边缘或 YZ 边缘都会返回非循环锦标赛。因此,在这种情况下,无法撤消转换。这意味着我们从这个比较器获得的任何排序顺序都会使这两项不合适,因此我们的最大距离至少为 1。
此特殊情况的重要说明:这对应于比较器创建一个包含节点 X、Y 和 Z 的循环的锦标赛。具体而言,它将采用 X、Z、Y、X 的形式。问题是我们无法判断原始排序是(X,Z,Y),还是(Z,Y,X),还是(Y,X,Z),所以我们的最大距离为至少 2 个。
最后,假设我们有两个节点 X 和 Y 并在 outdeg(X) = k、outdeg(Y) = m 和 k ≥ m + 3 的情况下翻转边 XY。我们现在剩下在锦标赛中,两个节点的出度为 k - 1,两个节点的出度为 m + 1。但是在这四个节点中,可以保证只有一对可以翻转以产生非循环锦标赛。一种看待这一点的方法:取现在重复出度的四个节点;称它们为 X 和 Y(如上)以及 W 和 Z,假设我们有循环 X、W、Z、Y、X,其中唯一与原始翻转的边是(Y,X)。这个循环会是什么样子?好吧,因为 (X, W)、(W, Z) 和 (Z, Y) 是锦标赛中没有翻转的边,所以在原始锦标赛中,我们有 outdeg(X) > outdeg(W) > outdeg (Z) > 外度 (Y)。这意味着我们必须让 X 和 W 在新图中具有出度 k - 1 并且 Z 和 Y 在新图中具有出度 m + 1。因此,仅将边缘从 Y 翻转到 X 会将度数-(k-1) 节点之一的度数增加到 k,同时也会将度数-(m+1) 节点之一的度数降低到 m .
总结:
定理:有故障的比较器要么
- 表现得像一个真正的比较器,在这种情况下,我们交换了原始序列中的两个相邻元素,我们永远不知道是哪个。
- 恰好有一个长度为 3 的元素的循环,其原始顺序永远无法知道,或者
- 有一个长度为 4 的循环,在这种情况下,我们可以确定哪个比较被反转。
考虑到这一点,用以下方式重新定义您的问题似乎是合理的:
目标: 设计一种算法,在 O(n log n) 时间内,在给定错误比较器的情况下,对包含 n 个元素的列表执行以下操作之一比较两个固定元素 X 和 Y 时返回错误的结果:
- 完美排序列表。
- 完美排序列表,除了两个相邻的项目交换。
- 完美排序列表,三个相邻项目除外。
这是一种可能的算法,它基于快速排序在预期的 O(n log n) 时间内完成此操作。基本思想如下:我们运行或多或少的常规快速排序,在每个时间点检查是否找到了三角形。如果不是,那么我们要么是情况(1),要么是情况(2)。如果我们确实找到了一个三角形,我们看看我们是否可以确定哪个比较被颠倒了。如果可以,那么我们重新运行快速排序,除了我们在这个损坏的情况下“修复”比较器。如果我们不能,那么我们在情况 (3) 中并像往常一样完成快速排序。
我们将用于检测三角形的特定技术是这样工作的。从常规的普通快速排序开始:选择一个枢轴,将数组划分为小于枢轴的事物和大于枢轴的事物,然后递归地对两个较小的子数组进行排序。然而,在这样做之后,我们又做了一个步骤:假设我们正在排序的子数组中包含三个或更多元素,请查看枢轴 p 以及它之前和之后的元素(将这些 s、p、g 称为“更小”、“枢轴”和“更大”)。那么如果比较器说 s
假设在快速排序比较器的某个时刻确实比较了 X 和 Y,即不匹配的项目。我们假设 X
这里应该发生什么,假设比较器是诚实的,Y 会被发现大于 X,因此将被放入“更大”的子数组中。但是因为比较器是一个撒谎的说谎者,所以 Y 被放置到“较小的”子数组中。如果我们然后递归地对“较小”子数组和“较大”子数组进行排序,请考虑 Y 将在哪里结束。它在“较小”子数组中,但实际上比 X 大,这意味着它将比“较小”子数组中的所有内容都大。因此,Y 将出现在 X 之前。现在,查看“更大”子数组中的项目。有两种选择。第一个是在“真实”排序中,X 和 Y 之间至少有一个值。然后该值将出现在“更大”子数组中,因为它大于 X,尤其是“更大”子数组的第一个元素将比较小于 Y。这意味着 Y,然后是 X,然后是排序后紧跟在 X 之后的项目将形成一个三角形。另一种选择是 X 和 Y 在真正的排序顺序中是相邻的,我们永远不会发现这种情况(如上所述)。结合上述见解,这意味着
定理:假设我们运行快速排序,在对左右子数组进行递归排序后,我们查看由枢轴组成的三个项目,即它之前的项目,以及紧随其后的项目,看看它们是否形成三角形。那么如果这个算法检测到一个三角形,那么就存在一个三角形。此外,如果该算法未检测到三角形,则 (1) 不存在三角形或 (2) 确实存在三角形,但比较器从未应用于坏对 (X, Y),因此排序顺序是正确的.
说了这么多,我们可以说明完整的算法,在预期的 O(n log n) 时间内,尽可能最好地对数组进行排序。
function modifiedQuicksort(array, comparator):
if array has length 0 or 1, return.
pick a random pivot element from the array.
use the comparator to form subarrays smaller and greater based on
how elements compare against the pivot.
recursively apply modifiedQuicksort to those two arrays.
if the comparator finds a triangle formed from the last element of
smaller, the pivot, and the first element of greater, report those
three items as a triangle.
return smaller, pivot, greater.
function sortAsBestWeCan(array, comparator):
run modifiedQuicksort(array, comparator)
if it didn't report a triangle, return the result of the call.
otherwise, it reported a triangle A, B, C.
for each other item D:
if comparator(A, D) and comparator(D, B) or
comparator(B, D) and comparator(D, C) or
comparator(C, D) and comparator(D, A):
you have found a 4-cycle from A, B, C, and D.
detect which comparison is reversed.
use that knowledge plus the comparator and your favorite
O(n log n)-time sorting algorithm to perfectly sort
the input array.
otherwise, those three items are the only triangle, and the
array is sorted as well as it can be. return it.