【问题标题】:Generate all subset sums within a range faster than O((k+N) * 2^(N/2))?在比 O((k+N) * 2^(N/2)) 更快的范围内生成所有子集和?
【发布时间】:2011-03-08 10:51:48
【问题描述】:

有没有办法生成 all 的子集和 s1, s2, ..., sk 在 [A,B] 范围内的速度比 O((k+N)*2N/2) 快,其中 k 是 [A,乙]?请注意,只有在我们枚举了 [A,B] 内的所有子集和之后,k 才知道。

我目前正在使用修改后的Horowitz-Sahni 算法。例如,我首先将其调用为大于或等于 A 的最小总和,给我 s1。然后我再次调用它以获得大于 s1 的下一个最小总和,给我 s2。重复此过程,直到我们找到一个总和 sk+1 大于 B。在每次迭代之间重复大量计算,即使没有重建最初的两个 2N/2列表,那么有没有办法做得更好?

在我的问题中,N约为15,数字的量级在百万级,所以我没有考虑动态规划路线。

【问题讨论】:

    标签: algorithm optimization search theory complexity-theory


    【解决方案1】:

    检查 Wikipedia 上的子集总和。据我所知,它是已知最快的算法,运行时间为 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 之间的所有总和,并且只需要您解决一个子集总和问题以及对 A 到 B 范围内的 K 值的 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很大的话可以优化一下,比如把sum映射或者hashmapping到迭代器,这样任何给定的firstit都可以在secondit中找到合适的伙伴,减少运行时间。

    【讨论】:

    • 感谢您的快速回复!我在算法的内部循环中使用维基百科上描述的算法。但由于我正在寻找子集总和值的范围,而不仅仅是一个,我想知道是否对新的转折进行了一些优化。
    • 如果您要保存结束数组,您可以遍历它们以查找任意端点,而无需重新计算它们。
    • 我已经编辑了我的问题,希望能够就您的问题进行澄清。我的问题描述仍然模棱两可吗?
    • 非常感谢您对此的关注。我认为这是您算法正确性的反例(更有可能我只是误解了某些东西)...根据您的算法,我们保留大于我们的下限 A 的最佳总和 s_best。当它运行时,它测试两个子集总和 > = s_best 和
    • 我认为如果我们在算法运行期间保留一组在 [A,B] 中找到的所有和,仍然存在分支点(?)我的推理在哪里走错了路?
    【解决方案2】:

    可以在 O(N*2^(N/2)) 中执行此操作,使用类似于 Horowitz Sahni 的想法,但我们尝试进行一些优化以减少 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 中的集合,其与 this 的并集给出范围 [A,B] 中的和。这是 O(N*2^(N/2))。同时,查找 S2 中对应的集合是否在 [A,B] 范围内。这里的优化是结合循环。注意:这为您提供了集合的表示(根据 S2 中的两个索引),而不是集合本身。如果你想要所有的集合,这就变成了 O(K + N*2^(N/2)),其中 K 是集合的数量。

    可能会进行进一步优化,例如当 S2 的总和为负时,我们不考虑总和

    由于步骤 2,3,4 应该很清楚,我将进一步详细说明如何在 O(2^(N/2)) 时间内完成步骤 1。

    为此,我们使用Gray Codes 的概念。格雷码是一个二进制位模式序列,其中每个模式与前一个模式的差异正好为 一个 位。 示例:00 -&gt; 01 -&gt; 11 -&gt; 10 是一个 2 位的格雷码。

    有灰色代码可以遍历所有可能的 N/2 位数,这些代码可以迭代生成(参见我链接到的 wiki 页面),每个步骤在 O(1) 时间内(总共 O(2^(N /2)) 步骤),给定前一个位模式,即给定当前位模式,我们可以在 O(1) 时间内生成下一个位模式。

    这使我们能够形成所有子集和,方法是使用前一个和并通过添加或减去一个数字(对应于不同的位位置)来改变它以获得下一个和。

    【讨论】:

      【解决方案3】:

      如果您以正确的方式修改 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 是输出的长度。

      【讨论】:

      • 我建议不要同时排序的原因:同时排序+线性时间是cRlogR + cLlogL + L + R。只排序一个并进行二进制搜索是 cRlogR + c'LlogR,如果我的计算正确,它对于 c > c' > 1 会更小.通过根据之前看到的最小值和最大值等将区域缩减为二进制搜索,可以进行更多优化。
      • +1。有趣的!当 wiki 说 Horowitz-Sahni 是迄今为止发现的最好的时,我从未停止考虑摆脱日志因素。也许您应该详细说明链接列表共享并将其添加到此处/到 wiki/在某处发布。
      • 根据您的建议,我添加了对数据存储问题的说明。如果我也可以提个建议,您能在 SO 中更改您的网名吗?
      • 不幸的是,这个名字被卡住了,过去提到这个名字的对话和答案只会让偶然发现他们的人感到困惑。不过,谢谢你的建议。
      猜你喜欢
      • 2021-12-22
      • 1970-01-01
      • 1970-01-01
      • 2018-10-24
      • 1970-01-01
      • 2012-11-22
      • 2013-12-19
      • 2020-09-21
      • 1970-01-01
      相关资源
      最近更新 更多