k-最大双重选择

2 投票
2 回答
778 浏览
提问于 2025-04-18 17:07

想象一下,你有两个袋子(AB),里面分别有 NM 个球。每个球都有一个已知的数字值(利润)。你的任务是从中提取出一对球,使得它们的总利润最大(这个总利润是通过选择的两个球的值相乘得到的)。

最简单的选择显而易见:从 AB 中各选出一个值最大的球。

问题在于,当你需要给出第二个或第 k 个最佳选择时,就不那么简单了。按照之前的方法,你应该从 AB 中选择最大的球,但不能重复选择。

这个问题可以通过计算每一种可能的选择,进行排序来解决(以下是一个 Python 示例):

def solution(A,B,K):
    if K < 1:
        return 0
    pool = []
    for a in A:
        for b in B:
            pool.append(a*b)
    pool.sort(reverse=True)
    if K>len(pool):
        return 0
    return pool[K-1]

这种方法有效,但它的最坏时间复杂度是 O(N*M*Log(M*M)),我敢打赌还有更好的解决方案。

我找到了一种基于表格的解决方案,其中 AB 的元素按从高到低排序,每个值都有一个索引,表示下一个要从另一列测试的值。最开始这个表格看起来是这样的:

enter image description here

A 中的第一个元素是 25,它需要与 20index 2 select from b = 0)进行比较,所以 25*20=500 是第一个最大的选择。然后,增加索引进行检查,表格变成:

enter image description here enter image description here

利用这些索引,我们可以快速找到最佳选择的候选者:

25 * 20 = 500 #first from A and second from B
20 * 20 = 400 #second from A and first from B

我尝试编写这个解决方案的代码:

def solution(A,B,K):
    if K < 1:
        return 0
    sa = sorted(A,reverse=true)
    sb = sorted(B,reverse=true)

    for k in xrange(K):
        i = xfrom
        j = yfrom
        if i >= n and j >= n:
                ret = 0
                break
        best = None
        while i < n and j < n:
                selected = False
                #From left
                nexti = i
                nextj = sa[i][1]
                a = sa[nexti][0]
                b = sb[nextj][0]
                if best is None or best[2]<a*b:
                        selected = True
                        best = [nexti,nextj,a*b,'l']
                #From right
                nexti = sb[j][1]
                nextj = j
                a = sa[nexti][0]
                b = sb[nextj][0]
                if best is None or best[2]<a*b:
                        selected = True
                        best = [nexti,nextj,a*b,'r']
                #Keep looking?
                if not selected or abs(best[0]-best[1])<2:
                        break
                i = min(best[:2])+1
                j = i
                print("Continue with: ", best, selected,i,j)
        #go,go,go
        print(best)
        if best[3] == 'l':
            dx[best[0]][1] = best[1]+1
            dy[best[1]][1] += 1
        else:
            dx[best[0]][1] += 1
            dy[best[1]][1] = best[0]+1
        if dx[best[0]][1]>= n:
            xfrom = best[0]+1
        if dy[best[1]][1]>= n:
            yfrom = best[1]+1
        ret = best[2]

    return ret

但它在在线的 Codility 评测中没有通过(我提到过这是一个已经过期的 Codility 挑战的部分解决方案吗?Sillicium 2014)。

我的问题是:

  • 第二种方法是一个未完成的好方案吗?如果是这样,你能给我一些提示,告诉我可能缺少什么吗?
  • 你知道有什么更好的方法来解决这个问题吗?

2 个回答

1

我不太明白“有替换”的意思...

...但假设这其实和“如何找到第k大和的对?”是一样的,那么解决这个问题的关键是考虑一个叫做S的矩阵,这个矩阵包含了所有的和(或者在你的情况下是乘积),它是由A和B构成的(在排序之后)——这篇论文(由@EvgenyKluev提到)给出了这个线索。

(你需要的是A*B而不是A+B……但结果是一样的——虽然负数会让事情变得复杂,但我认为这并不影响这个方法的有效性。)

下面是一个例子,展示了事情是如何运作的:

  for A = (2, 3, 5, 8, 13)
  and B = (4, 8, 12, 16)

我们有一个(假想的)数组S,其中S[r, c] = A[r] + B[c],在这个例子中:

   6 ( 2+4),  10 ( 2+8),  14 ( 2+12),  18 ( 2+16)
   7 ( 3+4),  11 ( 3+8),  15 ( 3+12),  19 ( 3+16)
   9 ( 5+4),  13 ( 5+8),  17 ( 5+12),  21 ( 5+16)
  12 ( 8+4),  16 ( 8+8),  20 ( 8+12),  14 ( 8+16)
  17 (13+4),  21 (13+8),  25 (13+12),  29 (13+16)

(正如引用的论文所指出的,我们并不需要构建数组S,我们可以在需要的时候生成S中的某个值。)

最有趣的是,S的每一列的值都是按升序排列的(当然),所以我们可以通过合并这些列(从底部读取)来按降序提取S中的值。

当然,合并这些列可以使用优先队列(堆)来完成——这就是最大堆的解决方案。最简单的方法是用S的底行来初始化堆,并标记每个堆中的项目来自哪个列。然后弹出堆顶的元素,并从刚刚弹出的同一列中推入下一个元素,直到你弹出第k个元素。(因为底行是排序的,所以用它来初始化堆是很简单的。)

这个过程的复杂度是O(k log n)——其中'n'是列的数量。这个方法处理行也同样有效……所以如果有'm'行和'n'列,你可以选择较小的那个!

注意:复杂度不是O(k log k)……而且对于给定的A和B,'n'是常量,所以O(k log n)实际上是O(k)!!

如果你想对不同的'k'进行多次探测,那么一个技巧是偶尔缓存这个过程的状态,这样未来的'k'可以从最近的检查点重新开始。在极限情况下,可以将合并过程完成并存储所有可能的值,以便O(1)查找!

2

你需要维护一个优先队列。

你从 (sa[0], sb[0]) 开始,然后继续到 (sa[0], sb[1])(sa[1], sb[0])。如果 (sa[0] * sb[1]) > (sa[1] * sb[0]),我们能否对 (sa[0], sb[2])(sa[1], sb[0]) 的大小关系做出任何判断呢?

答案是否定的。因此,我们必须维护一个优先队列。在每次移除队列中最大的 (sa[i], sb[j]) 之后,我们需要将 (sa[i - 1], sb[j])(sa[i], sb[j - 1]) 加入优先队列,并重复这个过程 k 次。

顺便提一下,我在回答一个 不同问题 时也提到了这个算法。这个算法乍一看可能和之前的不同,但其实它解决的是同一个问题。

撰写回答