如果您以正确的方式修改 Horowitz-Sahni 算法,那么它几乎不会比原来的 Horowitz-Sahni 慢。回想一下,Horowitz-Sahni 处理两个子集总和列表:原始列表左半部分的子集总和和右半部分的子集总和。调用这两个和 L 和 R 的列表。要获得总和为某个固定值 A 的子集,您可以对 R 进行排序,然后使用二分搜索在 R 中查找与 L 中的每个数字匹配的数字。然而,该算法是不对称的,只是为了节省空间和时间的常数因子。对于这个问题,对 L 和 R 都进行排序是个好主意。
在我下面的代码中,我还反转了 L。然后您可以保留两个指向 R 的指针,为 L 中的每个条目更新:一个指向 R 中太低的最后一个条目的指针,以及一个指向 R 中第一个条目的指针太高。当您前进到 L 中的下一个条目时,每个指针可能会向前移动或保持原位,但它们不必向后移动。因此,Horowitz-Sahni 算法的第二阶段只在第一阶段生成的数据中占用线性时间,加上输出长度上的线性时间。在一个恒定的因素下,你不能做得比这更好(一旦你致力于这种中间相遇算法)。
这是一个带有示例输入的 Python 代码:
# Input
terms = [29371, 108810, 124019, 267363, 298330, 368607,
438140, 453243, 515250, 575143, 695146, 840979, 868052, 999760]
(A,B) = (500000,600000)
# Subset iterator stolen from Sage
def subsets(X):
yield []; pairs = []
for x in X:
pairs.append((2**len(pairs),x))
for w in xrange(2**(len(pairs)-1), 2**(len(pairs))):
yield [x for m, x in pairs if m & w]
# Modified Horowitz-Sahni with toolow and toohigh indices
L = sorted([(sum(S),S) for S in subsets(terms[:len(terms)/2])])
R = sorted([(sum(S),S) for S in subsets(terms[len(terms)/2:])])
(toolow,toohigh) = (-1,0)
for (Lsum,S) in reversed(L):
while R[toolow+1][0] < A-Lsum and toolow < len(R)-1: toolow += 1
while R[toohigh][0] <= B-Lsum and toohigh < len(R): toohigh += 1
for n in xrange(toolow+1,toohigh):
print '+'.join(map(str,S+R[n][1])),'=',sum(S+R[n][1])
“白痴”(我认为他应该更改他的用户名)提出了通过跳过其中一种来进一步优化算法的合理问题。实际上,因为每个列表 L 和 R 都是子集大小的列表,所以您可以在线性时间内对每个列表进行组合生成和排序! (也就是说,列表的长度是线性的。)L 是两个总和列表的并集,一个包含第一项,term[0],另一个不包含。所以实际上你应该只以排序形式制作其中一个,添加一个常量,然后合并两个排序列表。如果你递归地应用这个想法,你会在时间中保存一个对数因子来生成一个排序的 L,即问题的原始变量中的 N 的因子。这为您在生成这两个列表时对它们进行排序提供了一个很好的理由。如果你只对一个列表进行排序,你有一些二分搜索可以重新引入 N 的那个因素;充其量你必须以某种方式优化它们。
乍一看,O(N) 的因子可能仍然存在,原因不同:如果您不仅想要子集总和,还想要求和的子集,那么它看起来像 O(N) 时间和将每个子集存储在 L 和 R 中的空间。但是,有一个数据共享技巧也可以摆脱 O(N) 的因素。该技巧的第一步是将左半部分或右半部分的每个子集存储为位的链接列表(如果包含项则为 1,如果不包含项则为 0)。然后,当列表 L 的大小像上一段一样增加一倍时,子集及其伙伴的两个链表可以共享,但头部除外:
0
|
v
1 -> 1 -> 0 -> ...
实际上,这个链表技巧是成本模型的产物,从来没有真正有用。因为,为了在 RAM 架构中获得 O(1) 成本的指针,您必须使用 O(log(memory)) 位定义数据字。但是,如果您有这种大小的数据字,您最好将每个字存储为单个位向量,而不是使用此指针结构。即,如果您需要的内存少于千兆字,则可以将每个子集存储在 32 位字中。如果您需要的不仅仅是一个千兆字,那么您有一个 64 位架构或它的仿真(或者可能是 48 位),您仍然可以将每个子集存储在一个字中。如果你修补 RAM 成本模型以考虑字长,那么 N 的这个因素永远不会真正存在。
因此,有趣的是,原始 Horowitz-Sahni 算法的时间复杂度不是 O(N*2^(N/2)),而是 O(2^(N/2))。同样,这个问题的时间复杂度是 O(K+2^(N/2)),其中 K 是输出的长度。