如何找到与第k个最大和的对?

时间:2013-09-01 09:44:36

标签: performance algorithm math language-agnostic combinatorics

给定两个排序的数字数组,我们希望找到具有第k个最大可能总和的对。 (一对是第一个数组中的一个元素,第二个数组中是一个元素)。例如,使用数组

  • [2,3,5,8,13]
  • [4,8,12,16]

总和最大的对是

  • 13 + 16 = 29
  • 13 + 12 = 25
  • 8 + 16 = 24
  • 13 + 8 = 21
  • 8 + 12 = 20

所以第四大总和是(13,8)。如何找到具有第k个最大可能总和的对?

另外,最快的算法是什么?数组已经排序,大小为M和N.


我已经知道 O(Klogk)解决方案,使用给定here的Max-Heap。

它也是最受欢迎的 Google 面试问题之一,他们需要 O(k)解决方案

我还在某处读到存在 O(k)解决方案,我无法弄清楚。

有人可以用伪代码解释正确的解决方案。

P.S。 请不要发布this链接作为回答/评论。它不包含答案。

6 个答案:

答案 0 :(得分:11)

我从一个简单但不太线性的算法开始。我们在array1[0]+array2[0]array1[N-1]+array2[N-1]之间选择了一些值。然后我们确定有多少对和大于这个值,以及有多少对更少。这可以通过用两个指针迭代数组来完成:当sum太大时,指向第一个数组的指针递增,当sum太小时,指向第二个数组的指针递减。对不同的值重复此过程并使用二分搜索(或单侧二分搜索),我们可以在O(N log R)时间内找到第K个最大和,其中N是最大数组的大小,R是{之间的可能值的数量。 {1}}和array1[N-1]+array2[N-1]。只有当数组元素是由小常数限定的整数时,该算法才具有线性时间复杂度。

如果我们在二进制搜索范围内的一对和的数量从O(N 2 )减少到O(N)时停止二进制搜索,则可以改进先前的算法。然后我们用这些对和填充辅助数组(这可以用略微修改的双指针算法完成)。然后我们使用quickselect算法在这个辅助数组中找到第K个最大和。所有这些都没有改善最坏情况的复杂性,因为我们仍然需要O(log R)二进制搜索步骤。如果我们保留此算法的quickselect部分但是(为了获得适当的值范围)我们使用比二分搜索更好的东西怎么办?

我们可以使用以下技巧估计值范围:从每个数组中获取每个第二个元素,并尝试为这些半数组找到具有等级array1[0]+array2[0]的对和(递归地使用相同的算法)。显然,这应该给出所需值范围的一些近似值。事实上,这种技巧的略微改进的变体给出了仅包含O(N)元素的范围。这在以下论文中得到证实:"Selection in X + Y and matrices with sorted rows and columns" by A. Mirzaian and E. Arjomandi。除了Quickselect之外,本文还详细解释了算法的所有部分的算法,证明,复杂度分析和伪代码。如果需要线性最坏情况复杂度,可以使用Median of medians算法扩充Quickselect。

该算法具有复杂度O(N)。如果其中一个阵列比其他阵列短(M

如果k < N我们可以忽略索引大于k的所有数组元素。在这种情况下,复杂度等于O(k)。如果N < k&lt; N(N-1)我们只有比OP中要求的更复杂。如果k> N(N-1),我们最好解决相反的问题:k'最小和。

我将简单的C ++ 11实现上传到ideone。代码未经优化且未经过全面测试。我试图尽可能接近链接纸张中的伪代码。此实现使用k/4,它仅允许平均线性复杂度(非最坏情况)。


在线性时间内找到第K个和的完全不同的方法是基于优先级队列(PQ)。一种变化是将最大的对插入PQ,然后重复移除PQ的顶部,而是插入最多两对(一个在一个数组中具有递减的索引,另一个在其他数组中具有递减的索引)。并采取一些措施,以防止插入重复对。其他变体是插入包含第一个数组的最大元素的所有可能的对,然后重复移除PQ的顶部,而是插入第一个数组中的递减索引和第二个数组中的相同索引的对。在这种情况下,没有必要打扰重复。

OP提到O(K log K)解决方案,其中PQ实现为max-heap。但在某些情况下(当数组元素是均匀分布的整数,只有平均而非最坏情况需要有限范围和线性复杂度)时,我们可以使用O(1)时间优先级队列,例如,如本文所述:{ {3}}。这允许O(K)预期的时间复杂度。

这种方法的优点是可以按排序顺序提供前K个元素。缺点是数组元素类型的选择有限,算法更复杂和更慢,渐近复杂度更差:O(K)> O(N)。

答案 1 :(得分:0)

编辑:这不起作用。我留下答案,因为显然我不是唯一一个能有这种想法的人;见下面的讨论。 反例是x =(2,3,6),y =(1,4,5)和k = 3,其中算法给出7(3 + 4)而不是8(3 + 5)。


xy成为两个数组,按递减顺序排序;我们想构建K - 最大的总和。

变量是:i第一个数组中的索引(元素x[i]),j第二个数组中的索引(元素y[j])和{{ 1}}&#34; order&#34;总和(k中的k),1..K将成为满足条件的S(k)=x[i]+y[j] - 更大的总和(这是循环不变量)。< / p>

k开始等于(i, j):显然,(0, 0)

S(1) = x[0]+y[0]k的{​​{1}}

,执行:

  • 如果1,则K-1(和x[i+1]+ y[j] > x[i] + y[j+1]不会更改);别的i := i+1

要了解它是否有效,请考虑您有j。然后,j:=j+1是低于(或等于)S(k) = x[i] + y[j]的最大总和,例如至少一个元素(S(k+1)S(k))更改。不难发现ij中只有一个应该改变。 如果i发生更改,则您可以构建的总和低于j的总和是设置i,因为S(k)正在减少,所有i=i+1都会{ {1}}大于x。同样适用于x[i'] + y[j],表明i' < iS(k)j

因此,在循环结束时,您找到了S(k+1) - 更大的总和。

答案 2 :(得分:0)

tl; dr:如果你向前看并在每次迭代后面看,你可以从结束开始(这是最高的)并在O(K)时间回来。

虽然我认为,这种方法背后的洞察力是合理的,但目前的代码并不完全正确(见评论)。


让我们看看:首先,数组是排序的。因此,如果数组是ab,其长度为MN,并且您已经安排了它们,则最大的项目位于广告位M和分别为N,最大的对将始终为a[M]+b[N]

现在,第二大对是什么?它可能会有{a[M],b[N]}中的一个(它不能同时具有两个,因为这只是最大的一对),并且至少有一个{a[M-1],b[N-1]}。但是,我们也知道如果我们选择a[M-1]+b[N-1],我们可以通过从同一个列表中选择更高的数字来使其中一个操作数更大,因此它将从最后一列中只有一个数字,而倒数第二列中有一个数字柱。

考虑以下两个数组:a = [1, 2, 53]; b = [66, 67, 68]。我们的最高对是53+68。如果我们失去了这两者中较小的一对,我们的对是68+2;如果我们失去了更大的,那就是53+67。因此,我们必须展望未来,以确定我们的下一对将是什么。最简单的超前策略就是计算两个可能对的总和。这将总是花费两次加成,并且每次转换需要两次比较(三次因为我们需要处理总和相等的情况);让我们称之为成本Q)。

起初,我很想重复K-1次。但是有一个障碍:下一个最大的一对可能实际上是我们可以从{{a[M],b[N]}, {a[M-1],b[N-1]}有效制作的另一对。所以,我们也需要回顾一下。

所以,让我们的代码(python,应该是2/3兼容):

def kth(a,b,k):
    M = len(a)
    N = len(b)
    if k > M*N:
       raise ValueError("There are only %s possible pairs; you asked for the %sth largest, which is impossible" % M*N,k)
    (ia,ib) = M-1,N-1 #0 based arrays
    # we need this for lookback
    nottakenindices = (0,0) # could be any value
    nottakensum = float('-inf')
    for i in range(k-1):
        optionone = a[ia]+b[ib-1]
        optiontwo = a[ia-1]+b[ib]
        biggest = max((optionone,optiontwo))
        #first deal with look behind
        if nottakensum > biggest:
           if optionone == biggest:
               newnottakenindices = (ia,ib-1)
           else: newnottakenindices = (ia-1,ib)
           ia,ib = nottakenindices
           nottakensum = biggest
           nottakenindices = newnottakenindices
        #deal with case where indices hit 0
        elif ia <= 0 and ib <= 0:
             ia = ib = 0
        elif ia <= 0:
            ib-=1
            ia = 0
            nottakensum = float('-inf')
        elif ib <= 0:
            ia-=1
            ib = 0
            nottakensum = float('-inf')
        #lookahead cases
        elif optionone > optiontwo: 
           #then choose the first option as our next pair
           nottakensum,nottakenindices = optiontwo,(ia-1,ib)
           ib-=1
        elif optionone < optiontwo: # choose the second
           nottakensum,nottakenindices = optionone,(ia,ib-1)
           ia-=1
        #next two cases apply if options are equal
        elif a[ia] > b[ib]:# drop the smallest
           nottakensum,nottakenindices = optiontwo,(ia-1,ib)
           ib-=1
        else: # might be equal or not - we can choose arbitrarily if equal
           nottakensum,nottakenindices = optionone,(ia,ib-1)
           ia-=1
        #+2 - one for zero-based, one for skipping the 1st largest 
        data = (i+2,a[ia],b[ib],a[ia]+b[ib],ia,ib)
        narrative = "%sth largest pair is %s+%s=%s, with indices (%s,%s)" % data
        print (narrative) #this will work in both versions of python
        if ia <= 0 and ib <= 0:
           raise ValueError("Both arrays exhausted before Kth (%sth) pair reached"%data[0])
    return data, narrative

对于那些没有python的人,这里有一个想法:http://ideone.com/tfm2MA

最糟糕的是,我们在每次迭代中进行了5次比较,并进行了K-1次迭代,这意味着这是一种O(K)算法。

现在,有可能利用有关值之间差异的信息来优化这一点,但这可以实现目标。


这是一个参考实现(不是O(K),但总会有效,除非有一个极端情况,其中对具有相等的总和):

import itertools
def refkth(a,b,k):
    (rightia,righta),(rightib,rightb) = sorted(itertools.product(enumerate(a),enumerate(b)), key=lamba((ia,ea),(ib,eb):ea+eb)[k-1]
    data = k,righta,rightb,righta+rightb,rightia,rightib
    narrative = "%sth largest pair is %s+%s=%s, with indices (%s,%s)" % data
    print (narrative) #this will work in both versions of python
    return data, narrative

这计算两个数组的笛卡尔乘积(即所有可能的对),用sum对它们进行排序,并取第k个元素。 enumerate函数用其索引装饰每个项目。

答案 3 :(得分:0)

如果最后两个解是在(a1,b1),(a2,b2),那么在我看来只有四个候选解(a1-1,b1)(a1,b1-1)(a2- 1,b2)(a2,b2-1)。这种直觉可能是错误的。当然每个坐标最多有四个候选者,而下一个最高者是16对中的一个({a1,a2,a1-1,a2-1}中的a,{b1,b2,b1-1,b2- 1})。那是O(k)。

(不,不,仍然不确定这是否可能。)

答案 4 :(得分:0)

另一个问题中的最大堆算法简单,快速且正确。不要敲它。这也很好解释了。 https://stackoverflow.com/a/5212618/284795

可能没有任何O(k)算法。没关系,O(k log k)几乎一样快。

答案 5 :(得分:0)

[2, 3, 5, 8, 13]
[4, 8, 12, 16]

合并2个数组并记下已排序数组中的索引。这是索引数组的样子(从1开始不是0)

[1,2,4,6,8] [3,5,7,9]

现在从头开始并制作元组。求和元组中的元素并选择第k个最大和。