k-最大双重选择
想象一下,你有两个袋子(A
和 B
),里面分别有 N
和 M
个球。每个球都有一个已知的数字值(利润)。你的任务是从中提取出一对球,使得它们的总利润最大(这个总利润是通过选择的两个球的值相乘得到的)。
最简单的选择显而易见:从 A
和 B
中各选出一个值最大的球。
问题在于,当你需要给出第二个或第 k 个最佳选择时,就不那么简单了。按照之前的方法,你应该从 A
和 B
中选择最大的球,但不能重复选择。
这个问题可以通过计算每一种可能的选择,进行排序来解决(以下是一个 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))
,我敢打赌还有更好的解决方案。
我找到了一种基于表格的解决方案,其中 A
和 B
的元素按从高到低排序,每个值都有一个索引,表示下一个要从另一列测试的值。最开始这个表格看起来是这样的:
A
中的第一个元素是 25
,它需要与 20
(index 2 select from b = 0
)进行比较,所以 25*20=500
是第一个最大的选择。然后,增加索引进行检查,表格变成:
利用这些索引,我们可以快速找到最佳选择的候选者:
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 个回答
我不太明白“有替换”的意思...
...但假设这其实和“如何找到第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)查找!
你需要维护一个优先队列。
你从 (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
次。
顺便提一下,我在回答一个 不同问题 时也提到了这个算法。这个算法乍一看可能和之前的不同,但其实它解决的是同一个问题。