有没有办法生成所有子集和s 1 ,s 2 ,...,s k 落在[A,B]范围内的速度比O((k + N)* 2 N / 2 更快),其中k是[A中的和的数量] B]?请注意,只有在我们枚举了[A,B]中的所有子集和之后才知道k。
我目前正在使用修改后的Horowitz-Sahni算法。例如,我首先调用它来获得大于或等于A的最小和,给我s 1 。然后我再次调用它以获得大于s 1 的下一个最小和,给出s 2 。重复这一步,直到我们发现一个和s k + 1 大于B.在每次迭代之间重复计算很多,即使没有重建最初的两个2 N / 2 列表,有没有办法做得更好?
在我的问题中,N大约是15,并且数字的大小在数百万的数量级,所以我没有考虑动态编程路线。
答案 0 :(得分:3)
检查维基百科上的子集和。据我所知,它是已知最快的算法,在O(2 ^(N / 2))时间内运行。
编辑: 如果你正在寻找多个可能的和,而不是只有0,你可以保存结束数组并再次迭代它们(这大致是O(2 ^(n / 2)操作)并保存重新计算它们。所有可能子集的值都不会随目标而变化。
再次编辑: 我不完全确定你想要什么。我们每次运行K搜索一个独立值,还是寻找具有K宽特定范围值的任何子集?或者您是否尝试使用第一个来近似第二个?
编辑回复: 是的,即使没有重建列表,你也会得到很多重复的工作。但是如果你不重建列表,那就不是O(k * N * 2 ^(N / 2))。构建列表是O(N * 2 ^(N / 2))。
如果您现在知道A和B,您可以开始迭代,然后在找到正确答案(底部边界)时不停止,但继续前进直到超出范围。这应该与仅解决一个解决方案的子集和大致相同,仅涉及+ k个操作,当你完成时,你可以抛弃列表。
更多编辑: 你有一系列的总和,从A到B.首先,你解决了A的子集和问题。然后,你只是继续迭代并存储结果,直到你找到B的解,然后停止。现在你在一次运行中得到A和B之间的每一个和,它只会花费你一个子集求和问题求解加上K值在A到B范围内的K值,这是线性的,又好又快。
s = *i + *j; if s > B then ++i; else if s < A then ++j; else { print s; ... what_goes_here? ... }
不,不,不。我现在得到了你的困惑的来源(我误读了一些东西),但它仍然没有你原来那么复杂。如果你想找到范围内的所有组合而不是一个,你只需要迭代两个列表的所有组合,这不是太糟糕。
请原谅我使用汽车。 C ++ 0x编译器。
std::vector<int> sums;
std::vector<int> firstlist;
std::vector<int> secondlist;
// Fill in first/secondlist.
std::sort(firstlist.begin(), firstlist.end());
std::sort(secondlist.begin(), secondlist.end());
auto firstit = firstlist.begin();
auto secondit = secondlist.begin();
// Since we want all in a range, rather than just the first, we need to check all combinations. Horowitz/Sahni is only designed to find one.
for(; firstit != firstlist.end(); firstit++) {
for(; secondit = secondlist.end(); secondit++) {
int sum = *firstit + *secondit;
if (sum > A && sum < B)
sums.push_back(sum);
}
}
它仍然不是很好。但是如果事先知道N非常大,例如将映射或散列映射到迭代器的总和,那么它可以被优化,这样任何给定的firstit都可以在secondit中找到任何合适的伙伴,从而减少运行时间。
答案 1 :(得分:2)
可以使用类似于Horowitz Sahni的想法在O(N * 2 ^(N / 2))中执行此操作,但我们尝试进行一些优化以减少BigOh中的常量。
我们执行以下操作
Step 1
:分成N / 2组,并为每个分组生成所有可能的2 ^(N / 2)组。称他们为S1和S2。我们可以在O(2 ^(N / 2))中做(注意:由于我们可以做的优化,这里缺少N因子)。
Step 2
:接下来在O(N * 2 ^(N / 2))时间内对S1和S2(比如S1)中的较大者进行排序(我们在这里通过不对两者进行优化来优化)。
Step 3
:使用二分搜索(因为它已排序)在S1中查找范围[A,B]中的子集总和。
Step 4
:接下来,对于S2中的每个和,使用二进制搜索查找S1中的集合,其与此的并集给出范围[A,B]中的和。这是O(N * 2 ^(N / 2))。同时,查找S2中的对应集合是否在[A,B]范围内。这里的优化是组合循环。注意:这为您提供了集合的表示(根据S2中的两个索引),而不是集合本身。如果你想要所有的集,那就变成O(K + N * 2 ^(N / 2)),其中K是集的数量。
进一步优化可能是可能的,例如当来自S2的总和是负数时,我们不考虑总和&lt;等等。
由于步骤2,3,4应该非常清楚,我将进一步阐述如何在O(2 ^(N / 2))时间内完成步骤1。
为此,我们使用Gray Codes的概念。格雷码是一系列二进制位模式,其中每个模式与前一个模式完全不同,只有一个位。
示例:00 -> 01 -> 11 -> 10
是一个带2位的格雷码。
有灰色代码遍历所有可能的N / 2位数,这些可以迭代生成(参见我链接到的wiki页面),每个步骤的O(1)时间(总O(2 ^(N) / 2))步骤),给定前一位模式,即给定当前位模式,我们可以在O(1)时间内生成下一位模式。
这使我们能够通过使用前面的和来形成所有子集和,并通过添加或减去一个数字(对应于不同的位位置)来改变它,以获得下一个和。
答案 2 :(得分:0)
如果你以正确的方式修改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])
“Moron”(我认为他应该改变他的用户名)通过跳过其中一种来提出进一步优化算法的合理问题。实际上,因为每个列表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(内存))位定义数据字。但是如果你有这样大小的数据字,你也可以将每个字存储为单个位向量而不是这个指针结构。即,如果您需要少于千兆字节的内存,那么您可以将每个子集存储在32位字中。如果您需要的不仅仅是千兆字节,那么您拥有64位架构或其仿真(或者可能是48位),您仍然可以将每个子集存储在一个字中。如果你修改RAM成本模型来考虑字数大小,那么N的这个因素从来就不存在。
因此,有趣的是,原始Horowitz-Sahni算法的时间复杂度不是O(N * 2 ^(N / 2)),它是O(2 ^(N / 2))。同样,这个问题的时间复杂度是O(K + 2 ^(N / 2)),其中K是输出的长度。