快速解决子集和

时间:2012-03-21 17:06:41

标签: algorithm subset-sum

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

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

6 个答案:

答案 0 :(得分:16)

我尊重您尝试解决此问题的快捷方式!不幸的是,你重新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 referencefurther reading on CSTheory.SE

答案 1 :(得分:7)

我不太了解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 :(得分:6)

虽然my previous answer描述了the polytime approximate algorithm to this problem,,但当所有 x i 时,{em>具体Pisinger's polytime dynamic programming solution的实施做出了 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子问题的最佳解决方案。

答案 3 :(得分:3)

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

答案 4 :(得分:2)

以下三种方法可以提高代码效率:

  1. 代码存储每个部分和的活动列表。在内存和时间方面,只需存储实现总和所需的最新活动,并在找到解决方案后通过回溯计算其余活动,效率更高。

  2. 对于每个活动,字典将使用旧内容(子集[prev_sum] =子集)重新填充。简单地生成单个字典

  3. 更快
  4. 将值拆分为两个并在中间方法中应用符号。

  5. 应用前两个优化会产生以下代码,速度提高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定义为元素的最大绝对值。 在中间方法中满足的算法益处在很大程度上取决于界限。

    对于下限(例如,约束= 1000和n = 300),除了第一改进方法之外,中间的相遇仅获得约2改善的因子。这是因为称为子集的字典是密集的。

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

    对于上限,中间的相遇将取正常方法的操作数的平方根。

    看起来令人惊讶的是,在中间遇到的速度只是下限的两倍。原因是每次迭代中的操作数取决于字典中的键数。添加k个活动之后,我们可能会期望有2 ** k个键,但如果bound很小,那么这些键中的许多会发生冲突,所以我们只会使用O(bound.k)键。

答案 5 :(得分:0)

我认为我会分享我在维基百科中描述的伪多边形算法的Scala解决方案。这是一个略微修改的版本:它计算出有多少独特的子集。这与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)        

    }
}