快速解决子集和

Posted

技术标签:

【中文标题】快速解决子集和【英文标题】:Fast solution to Subset sum 【发布时间】:2012-04-06 06:08:51 【问题描述】:

考虑这种解决子集和问题的方法:

def subset_summing_to_zero (activities):
  subsets = 0: []
  for (activity, cost) in activities.iteritems():
      old_subsets = subsets
      subsets = 
      for (prev_sum, subset) in old_subsets.iteritems():
          subsets[prev_sum] = subset
          new_sum = prev_sum + cost
          new_subset = subset + [activity]
          if 0 == new_sum:
              new_subset.sort()
              return new_subset
          else:
              subsets[new_sum] = new_subset
  return []

我从这里得到它:

http://news.ycombinator.com/item?id=2267392

还有一条评论说可以让它“更高效”。

怎么做?

另外,有没有其他方法可以解决这个问题,至少和上面的一样快?

编辑

我对任何可以加快速度的想法都感兴趣。我发现:

https://en.wikipedia.org/wiki/Subset_sum_problem#cite_note-Pisinger09-2

其中提到了线性时间算法。但是我没有那张纸,也许你们,亲爱的人们,知道它是如何工作的吗?也许是一个实现?也许完全不同的方法?

编辑 2

现在有后续了:Fast solution to Subset sum algorithm by Pisinger

【问题讨论】:

可以下载论文from here。 真的是同一张纸吗?他们有不同的头衔..? 摘要声称“将动态规划算法限制为仅考虑平衡状态,这意味着子集-子问题 [...] 可以在线性时间内解决,前提是系数受常数限制.",这就是我认为您正在寻找的内容。 除非 P=NP,否则没有线性时间算法,因为子集和是 NP 完全的。您的算法和我的答案是线性时间,当权重有界时 - 这意味着子集数组中只有 O(activities.size() * 2^bound) 个可能的元素。 @maniek 自言自语——因为我不能再编辑评论了。大小实际上是 O(activities.size() * bound),而不是 O(activities.size() * 2^bound) 【参考方案1】:

我尊重您尝试解决此问题的积极性!不幸的是,你是trying to solve a problem that's NP-complete,这意味着任何突破多项式时间障碍的进一步改进都将prove that P = NP。

您从 Hacker News 中提取的实现似乎与 the pseudo-polytime dynamic programming solution 一致,根据定义,任何额外的改进都必须推进当前对该问题及其所有算法异构体的研究状态。换句话说:虽然可以实现恒定的加速,但您非常不太可能在此线程的上下文中看到此问题解决方案的算法改进。

但是,如果您需要具有可容忍错误程度的多时间解决方案,您可以use an approximate algorithm。在从***公然窃取的伪代码中,这将是:

initialize a list S to contain one element 0.
 for each i from 1 to N do
   let T be a list consisting of xi + y, for all y in S
   let U be the union of T and S
   sort U
   make S empty 
   let y be the smallest element of U 
   add y to S 
   for each element z of U in increasing order do
      //trim the list by eliminating numbers close to one another
      //and throw out elements greater than s
     if y + cs/N < z ≤ s, set y = z and add z to S 
 if S contains a number between (1 − c)s and s, output yes, otherwise no

Python 实现,尽可能地保留原始术语:

from bisect import bisect

def ssum(X,c,s):
    """ Simple impl. of the polytime approximate subset sum algorithm 
    Returns True if the subset exists within our given error; False otherwise 
    """
    S = [0]
    N = len(X)
    for xi in X:
        T = [xi + y for y in S]
        U = set().union(T,S)
        U = sorted(U) # Coercion to list
        S = []
        y = U[0]
        S.append(y)
        for z in U: 
            if y + (c*s)/N < z and z <= s:
                y = z
                S.append(z)
    if not c: # For zero error, check equivalence
        return S[bisect(S,s)-1] == s
    return bisect(S,(1-c)*s) != bisect(S,s)

... 其中 X 是您的术语包,c 是您的精度(介于 0 和 1 之间),而 s 是目标总和。

更多详情请见the Wikipedia article。

(Additional reference, further reading on CSTheory.SE)

【讨论】:

谢谢!上面链接的 Pisinger 的论文说,有一个运行 O(nc) 的动态编程算法——我想这就是 Hacker News 的方法。但是他们说,如果权重是“有界的”,则复杂度可以写为 O(n^2*r),并且他们提出了一种在 O(nr) 中运行的算法。他们甚至为它提供了伪代码(第 4 页),但不幸的是,这有点超出我的想象。 然而,我想我会更高兴有一个精确的解决方案。顺便说一句,仅供参考,如果我们谈论我曾经发现的近似方法:en.wikipedia.org/wiki/Polynomial-time_approximation_scheme @EcirHana 会的!我现在需要检查一下,但我很乐意在今天晚些时候写一个 Python 版本。也感谢您在纸上提供的有用信息。 :) 非常感谢您的努力,但恐怕我们彼此误解了。我想要一个精确的解决方案,而不是近似的解决方案。我以为您的“会做”意味着您要查看论文-我会早点阻止您-对不起。 @EcirHana 一点都不麻烦!这需要一个 Python 实现供后代使用。不过,我会研究这篇论文,看看我是否可以对 DP 方法做出任何改进。没有承诺,但如果我写一个实现,它就会在这里。 :)【参考方案2】:

我对python了解不多,但是中间有个叫meet的方法。 伪代码:

Divide activities into two subarrays, A1 and A2
for both A1 and A2, calculate subsets hashes, H1 and H2, the way You do it in Your question.
for each (cost, a1) in H1
     if(H2.contains(-cost))
         return a1 + H2[-cost];

这将使您可以在合理时间内处理的活动元素数量增加一倍。

【讨论】:

非常聪明,谢谢!不幸的是,正如您所说,它“只是”将运行时间提高了一个常数因子(2 倍)。 假设您有数字 1,2,4,..2^39。你想检查一些数字。如果没有 MIM,您需要 2^40 次操作。有了它 - 2^20(假设哈希在 O(1) 中有效)【参考方案3】:

虽然my previous answer 描述了the polytime approximate algorithm to this problem,,但当xi 中的所有xi em>x 是正数:

from bisect import bisect

def balsub(X,c):
    """ Simple impl. of Pisinger's generalization of KP for subset sum problems
    satisfying xi >= 0, for all xi in X. Returns the state array "st", which may
    be used to determine if an optimal solution exists to this subproblem of SSP.
    """
    if not X:
        return False
    X = sorted(X)
    n = len(X)
    b = bisect(X,c)
    r = X[-1]
    w_sum = sum(X[:b])
    stm1 = 
    st = 
    for u in range(c-r+1,c+1):
        stm1[u] = 0
    for u in range(c+1,c+r+1):
        stm1[u] = 1
    stm1[w_sum] = b
    for t in range(b,n+1):
        for u in range(c-r+1,c+r+1):
            st[u] = stm1[u]
        for u in range(c-r+1,c+1):
            u_tick = u + X[t-1]
            st[u_tick] = max(st[u_tick],stm1[u])
        for u in reversed(range(c+1,c+X[t-1]+1)):
            for j in reversed(range(stm1[u],st[u])):
                u_tick = u - X[j-1]
                st[u_tick] = max(st[u_tick],j)
    return st

哇,真让人头疼。这需要校对,因为虽然它实现了balsub,但我无法定义正确的比较器来确定是否存在针对这个 SSP 子问题的最佳解决方案。

【讨论】:

如你所说,它不起作用。我认为,如果我们想尽可能近距离地阅读论文,两行应该看起来不同:for t in range(b,n+1):for j in reversed(range(stm1[u],st[u])):。但随后它因“列表索引超出范围”而中断,因此可能存在一个错误。无论如何,感谢您到目前为止的帮助! @EcirHana balsub 现已实施,它应该免费。由于描述简洁,我无法理解给定的 SSP 最佳解决方案z = maxu &lt;= c: sn(u) &gt; 0,但您也许可以在这方面取得进展。 :) 抱歉 - 这是什么或在哪里z = max... @EcirHana 论文第四页底部,balsub 伪代码之前的第二段。 即使现在这不起作用,您的回答也是最有帮助的,所以永恒的名声是您的:Eternal fame。我将专门发布有关此算法的另一个问题。谢谢!【参考方案4】:

我为“讨论”这个问题道歉,但是 x 值有界的“子集和”问题不是问题的 NP 版本。动态规划解决方案以有界 x 值问题而闻名。这是通过将 x 值表示为单位长度的总和来完成的。动态规划解决方案具有许多基本迭代,这些迭代与 x 的总长度成线性关系。但是,当数字的精度等于 N 时,子集总和为 NP。也就是说,表示 x 所需的数字或以 2 为底的位置值 = N。对于 N = 40,x 必须以十亿为单位。在 NP 问题中,x 的单位长度随 N 呈指数增长。这就是为什么动态规划解决方案不是 NP 子集和问题的多项式时间解决方案。在这种情况下,仍然存在 x 有界且动态规划解决方案有效的子集和问题的实际实例。

【讨论】:

完全欢迎您“讨论”这个问题,没有理由道歉。关于 NP 和动态规划,您是对的,我愿意/愿意牺牲一些一般性(例如,权重可以有界)来换取速度。也就是说,上面的论文声称比问题中的经典 DP 解决方案运行得更快(至少我是这么理解的)。另外,我希望从这里的人那里收集一些关于这个问题的技巧。【参考方案5】:

下面是三种让代码更高效的方法:

    代码存储每个部分总和的活动列表。仅存储求和所需的最新活动,并在找到解决方案后通过回溯来计算其余活动,在内存和时间方面都更有效。

    对于每个活动,字典都会重新填充旧内容(子集 [prev_sum] = 子集)。简单地增长一个字典会更快

    将值一分为二并在中间应用相遇方法。

应用前两个优化会导致以下代码快 5 倍以上:

def subset_summing_to_zero2 (activities):
  subsets = 0:-1
  for (activity, cost) in activities.iteritems():
      for prev_sum in subsets.keys():
          new_sum = prev_sum + cost
          if 0 == new_sum:
              new_subset = [activity]
              while prev_sum:
                  activity = subsets[prev_sum]
                  new_subset.append(activity)
                  prev_sum -= activities[activity]
              return sorted(new_subset)
          if new_sum in subsets: continue
          subsets[new_sum] = activity
  return []

同样应用第三个优化结果如下:

def subset_summing_to_zero3 (activities):
  A=activities.items()
  mid=len(A)//2
  def make_subsets(A):
      subsets = 0:-1
      for (activity, cost) in A:
          for prev_sum in subsets.keys():
              new_sum = prev_sum + cost
              if new_sum and new_sum in subsets: continue
              subsets[new_sum] = activity
      return subsets
  subsets = make_subsets(A[:mid])
  subsets2 = make_subsets(A[mid:])

  def follow_trail(new_subset,subsets,s):
      while s:
         activity = subsets[s]
         new_subset.append(activity)
         s -= activities[activity]

  new_subset=[]
  for s in subsets:
      if -s in subsets2:
          follow_trail(new_subset,subsets,s)
          follow_trail(new_subset,subsets2,-s)
          if len(new_subset):
              break
  return sorted(new_subset)

定义bound为元素的最大绝对值。 中间相遇方法的算法优势很大程度上取决于边界。

对于下限(例如,bound=1000 和 n=300),中间的相遇只得到了大约 2 倍的改进,而不是第一个改进的方法。这是因为称为子集的字典非常密集。

但是,对于高界限(例如 bound=100,000 和 n=30),中间相遇需要 0.03 秒,而第一个改进方法需要 2.5 秒(原始代码需要 18 秒)

对于上限,中间的相遇大约取普通方法操作次数的平方根。

在中间相遇的速度只有下界的两倍,这似乎令人惊讶。原因是每次迭代的操作数取决于字典中的键数。添加 k 个活动后,我们可能期望有 2**k 个键,但如果 bound 很小,那么其中许多键会发生冲突,因此我们将只有 O(bound.k) 个键。

【讨论】:

我专门写了“不依赖某些实现细节”。 #1 的不同之处在于 Python 在 O(n) 中运行 list + [item]。 #2 添加到哈希表是 O(1)。 #3 正是我正在寻找的那种技巧,见上文。【参考方案6】:

我想分享我的 Scala 解决方案,用于 wikipedia 中描述的讨论的伪多时间算法。这是一个稍微修改过的版本:它计算出有多少个独特的子集。这与https://www.hackerrank.com/challenges/functional-programming-the-sums-of-powers 中描述的 HackerRank 问题非常相关。编码风格可能不是很好,我还在学习 Scala :) 也许这对某人仍然有帮助。

object Solution extends App 
    var input = "1000\n2"

    System.setIn(new ByteArrayInputStream(input.getBytes()))        

    println(calculateNumberOfWays(readInt, readInt))

    def calculateNumberOfWays(X: Int, N: Int) = 
            val maxValue = Math.pow(X, 1.0/N).toInt

            val listOfValues = (1 until maxValue + 1).toList

            val listOfPowers = listOfValues.map(value => Math.pow(value, N).toInt)

            val lists = (0 until maxValue).toList.foldLeft(List(List(0)): List[List[Int]]) ((newList, i) => 
                    newList :+ (newList.last union (newList.last.map(y => y + listOfPowers.apply(i)).filter(z => z <= X)))
            )

            lists.last.count(_ == X)        

    

【讨论】:

以上是关于快速解决子集和的主要内容,如果未能解决你的问题,请参考以下文章

是否有一种快速算法可以将集合的所有分区生成为大小为 2 的子集(和一个大小为 1 的子集)?

Python:快速子集和循环数据框

Python:快速子集和循环数据框

Python 中 Pandas 的快速子集化

快速查找所有子集

子集和问题的这个变体更容易解决吗?