Быстрое решение для суммы подмножества - PullRequest
16 голосов
/ 21 марта 2012

Рассмотрим этот способ решения проблемы сумм подмножеств:

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

Теперь есть продолжение:
Быстрое решение алгоритма суммы подмножеств с помощью Pisinger

Ответы [ 6 ]

16 голосов
/ 29 марта 2012

Я уважаю готовность, с которой вы пытаетесь решить эту проблему! К сожалению, вы пытаетесь решить проблему, которая является NP-полной , что означает, что любое дальнейшее улучшение, которое преодолеет полиномиальный временной барьер, докажет, что P = NP .

Реализация, которую вы извлекли из Hacker News, похоже, согласуется с решением для динамического программирования с псевдополимедией , где любые дополнительные улучшения должны, по определению, улучшать состояние текущих исследований этой проблемы и все его алгоритмические изоформы. Другими словами: хотя возможно постоянное ускорение, вы очень вряд ли увидите алгоритмическое улучшение этого решения проблемы в контексте этой цепочки.

Однако, вы можете использовать приблизительный алгоритм , если вам требуется решение на основе множителя с допустимой степенью ошибки. В псевдокоде, явно украденном из Википедии, это будет:

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 - целевая сумма.

Подробнее см. статья в Википедии .

( Дополнительная справка , дальнейшее чтение на CSTheory.SE )

7 голосов
/ 26 марта 2012

Я не знаю много 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];

Это позволит вам удвоить количество элементов действий, которые вы можете выполнить в разумные сроки.

6 голосов
/ 31 марта 2012

В то время как мой предыдущий ответ описывает приближенный алгоритм polytime к этой проблеме, запрос был специально , сделанный для реализации многопланового динамического программирования Пизингера решение , когда все x i in 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 голосов
/ 01 апреля 2012

Я прошу прощения за "обсуждение" проблемы, но проблема "Подмножество сумм", где значения x ограничены, не является NP-версией проблемы.Решения динамического программирования известны для ограниченных задач x значения.Это делается путем представления значений х в виде суммы единиц длины.Решения динамического программирования имеют ряд фундаментальных итераций, которые линейны с общей длиной х.Тем не менее, Подмножество Sum находится в NP, когда точность чисел равна N. То есть число или базовые 2 места значения, необходимые для определения x, равно = N. Для N = 40 x должно быть в миллиардах.В задаче NP единичная длина x увеличивается экспоненциально с N. Поэтому решения динамического программирования не являются полиномиальным по времени решением задачи NP Subset Sum.В этом случае все еще существуют практические примеры проблемы суммы подмножеств, где x ограничены и решение для динамического программирования является действительным.

2 голосов
/ 30 марта 2012

Вот три способа сделать код более эффективным:

  1. Код хранит список действий для каждой частичной суммы. С точки зрения памяти и времени более эффективно просто хранить самые последние действия, необходимые для суммирования, и отрабатывать все остальное, возвращаясь назад, когда решение найдено.

  2. Для каждого действия словарь заполняется старым содержимым (подмножества [prev_sum] = подмножество). Быстрее просто вырастить один словарь

  3. Разделение значений на две части и применение встречи в середине.

Применение первых двух оптимизаций приводит к следующему коду, который более чем в 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)

Определить границу, равную наибольшему абсолютному значению элементов. Алгоритмическое преимущество подхода «посередине» во многом зависит от оценки.

Для нижней границы (например, связанной = 1000 и n = 300) встреча в середине получает только коэффициент улучшения примерно в 2 раза по сравнению с первым улучшенным методом. Это связано с тем, что словарь подмножеств заполнен.

Однако для верхней границы (например, bound = 100 000 и n = 30) встреча в середине занимает 0,03 секунды по сравнению с 2,5 секундами для первого улучшенного метода (и 18 секунд для исходного кода)

Для верхних границ встреча в середине займет примерно квадратный корень из числа операций обычного метода.

Может показаться удивительным, что встреча в середине только в два раза быстрее для низких границ. Причина в том, что количество операций в каждой итерации зависит от количества ключей в словаре. После добавления k действий мы могли бы ожидать, что будет 2 ** k ключей, но если привязка мала, то многие из этих ключей столкнутся, поэтому у нас будут только ключи O (bound.k).

0 голосов
/ 07 апреля 2014

Думаю, я бы поделился своим решением Scala для обсуждаемого псевдополимного алгоритма, описанного в Википедии.Это слегка измененная версия: она вычисляет, сколько существует уникальных подмножеств.Это очень сильно связано с проблемой HackerRank, описанной в https://www.hackerrank.com/challenges/functional-programming-the-sums-of-powers. Стиль кодирования может быть не очень хорошим, я все еще изучаю 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)        

    }
}
...