【问题标题】:How to divide a list of negative and positive numbers into the largest number of subsets whose sum is 0?如何将负数和正数列表划分为和为0的最大子集?
【发布时间】:2020-07-01 17:07:58
【问题描述】:

我正在尝试解决这个问题,但我不知道如何解决。

假设我有一个正负数列表,其总和保证为 0。

[-10, 1, 2, 20, 5, -100, -80, 10, 15, 15, 60, 100, -20, -18]                  

我想获得一个包含最多子集的列表,只使用初始列表的所有元素一次。并且每个子集的总和必须为 0。

所以在这个简单输入的情况下:

[-5, -4, 5, 2, 3, -1]

可以获得的最佳结果是:

1. [[-5, 5], [[-4, -1, 2, 3]]]   #2 subsets
2. [[-5, 2, 3], [-4, -1, 5]]     #2 subsets

例如,这些将是完全错误答案

1. [[-5, -4, -1, 2, 3, 5]]       #1 subsets that is the initial list, NO
2. [[-5,5]]                      #1 subset, and not all elements are used, NO

即使它是 NP-Complete,即使使用蛮力方法,我也如何设法解决它?我只需要一个小数字列表的解决方案。

【问题讨论】:

    标签: algorithm partition knapsack-problem subset-sum bin-packing


    【解决方案1】:
    def get_subsets(lst):
        N = len(lst)
        cand = []
        dp = [0 for x in range(1<<N)]   # maximum number of subsets using elements represented by bitset
        back = [0 for x in range(1<<N)]
    
        # Section 1
        for i in range(1,1<<N):
            cur = 0
            for j in range(N):
                if i&(1<<j):
                    cur += lst[j]
            if not cur:
                cand.append(i)     # if subset sums to 0, it's viable
        dp[0] = 1
    
        # Section 2
        for i in range(1<<N):
            while cand and cand[0] <= i:
                cand.pop(0)
            if not dp[i]:
                continue
            for j in cand:
                if i&j:     # if subsets intersect, it cannot be added
                    continue
                if dp[i]+1 > dp[i|j]:
                    back[i|j] = j
                    dp[i|j] = dp[i]+1
    
        # Section 3
        ind = dp.index(max(dp))
        res = []
    
        while back[ind]:
            cur = []
            for i in range(N):
                if back[ind]&(1<<i):
                    cur.append(lst[i])
            res.append(cur)
            ind = ind^back[ind]
    
        return res
    
    print (get_subsets([-5, -4, 5, 2, 3, -1]))
    

    基本上,此解决方案收集原始列表中所有总和为零的子集,然后尝试将尽可能多的子集合并在一起而不会发生冲突。它在最坏情况 O(2^{2N}) 时间内运行,其中 N 是列表的长度,但它应该达到 O(2^N) 左右的平均情况,因为通常不应该有太多子集总和为 0。

    编辑:我添加了部分以方便解释算法

    第 1 部分:我遍历原始列表的所有可能的 2^N-1 个非空子集,并检查这些子集中的哪些总和为 0;任何可行的零和子集都将添加到列表 cand(表示为 [1,2^N-1] 范围内的整数,并在构成子集的索引处设置位)。

    第 2 部分:dp 是一个动态规划表,它存储了使用整数 idp[i] 表示的子集可以形成的子集的最大数量和为 0。最初,dp 的所有条目都设置为 0,除了 dp[0] = 1,因为空集的总和为 0。然后我从 0 到 2^N-1 遍历每个子集,并遍历候选列表子集并尝试合并这两个子集。

    第 3 部分:这只是回溯寻找答案:在填写 dp 的同时,我还保留了一个数组 back,该数组存储了最近添加的子集,以在 back[i] 处实现子集 i。所以我找到了最大化子集数量的子集,使用ind = dp.index(max(dp)),然后我从那里回溯,通过删除最近添加的子集来缩小子集,直到我最终回到空集。

    【讨论】:

    • 你能解释一下它是如何工作的吗?它看起来很有趣,但我几乎无法理解。
    【解决方案2】:

    下面与 Michael Huang 的想法基本相同,多了 30 行...

    派系解决方案

    1. 我们可以预先构建所有总和为 0 的子集。

      • 构建 1 个元素的子集;
      • 然后通过重用以前的大小为 2
      • 并保留那些总和为零的人

    现在说这样的子集是图的一个节点。 然后一个节点与另一个节点相关,如果它们关联的子集没有共同的数字。

    1. 因此我们想要构建图的最大团:

      • 在一个 clique 中,所有节点都相互关联,它们的子集是不相交的
      • 最大集团给了我们最大的子集数

    function forall (v, reduce) {
      const nexts = v.map((el, i) => ({ v: [el], i, s: el })).reverse()
      while (nexts.length) {
        const next = nexts.pop()
        for (let i = next.i + 1; i < v.length; ++i) {
          const { s, skip } = reduce(next, v[i])
          if (!skip) {
            nexts.push({ v: next.v.concat(v[i]), s: s, i })
          }
        }
      }
    }
    function buildSubsets (numbers) {
      const sums = []
      forall(numbers, (next, el) => {
        const s = next.s + el
        if (s === 0) {
          sums.push({ s, v: next.v.concat(el) })
          return { s, skip: true }
        }
        return { s }
      })
      return sums
    }
    const bin2decs = bin => {
      const v = []
      const s = bin.toString(2)
      for (let i = 0; i < s.length; ++i) {
        if (intersects(dec2bin(i), bin)) {
          v.push(i)
        }
      }
      return v
    }
    const dec2bin = dec => Math.pow(2, dec)
    const decs2bin = decs => decs.reduce((bin, dec) => union(dec2bin(dec), bin), 0)
    // Set methods on int
    const isIn = (a, b) => (a & b) === a
    const intersects = (a, b) => a & b
    const union = (a, b) => a | b
    
    // if a subset contains another one, discard it
    // e.g [1,2,4] should be discarded if [1,2] is present
    const cleanSubsets = bins => bins.filter(big => bins.every(b => big === b || !isIn(b, big)))
    function bestClique (decs) {
      const cliques = []
      forall(decs, (next, el) => {
        if (intersects(next.s, el)) { return { skip: true } }
        const s = union(next.s, el)
        cliques.push({ s, v: next.v.concat(el) })
        return { s }
      })
      return cliques.sort((a, b) => b.v.length - a.v.length)[0]
    }
    // in case we have duplicated numbers in the list,
    // they are still uniq thanks to their id: i (idem position in the list)
    const buildNumbers = v => v.map((n, i) => {
      const u = new Number(n)
      u.i = i
      return u
    })
    function run (v) {
      const numbers = buildNumbers(v)
      const subs = buildSubsets(numbers)
      const bins = subs.map(s => decs2bin(s.v.map(n => n.i)))
      const clique = bestClique(cleanSubsets(bins))
      const indexedSubs = clique.v.map(bin2decs)
      const subsets = indexedSubs.map(sub => sub.map(i => numbers[i].valueOf()))
      console.log('subsets', JSON.stringify(subsets))
    }
    
    run([1, -1, 2, -2])
    run([-10, 1, 2, 20, 5, -100, -80, 10, 15, 15, 60, 100, -20, -18, 10, -10])
    run([-5, -4, 5, 2, 3, -1])

    【讨论】:

      【解决方案3】:

      这个问题是 NP 完全的,因为它是两个 NP 完全问题的组合:

      • 找到总和为0 的单个子集称为subset sum problem
      • 当您找到总和为0 的所有子集时,您必须解决具有特殊条件的exact cover problem:您希望最大化子集的数量。

      以下步骤将提供解决方案:

      几点说明:

      1. 首先,我们知道存在精确覆盖,因为数字列表的总和为 0。

      2. 其次,我们只能使用不是任何其他子集的超集的子集。因为,如果AX 的超集(两者之和为0),则A 不能在具有最多子集的封面中。让A, B, C, ... 是具有最大子集数的封面,然后我们可以将A 替换为XA\X(很容易看出总和A\X 元素中的 0) 我们得到了封面 X, A\X, B, C, ... 这样更好。

      3. 第三,当我们使用算法 X 时,搜索树中的所有路径都会导致成功。让A, B, C, ... 是由非重叠子集组成的路径,每个子集的总和为 0。那么补全也有0的总和(它可能是另一个子集的超集,然后我们将使用2。)。

      如您所见,这里没有什么新东西,我将只使用众所周知的技术/算法。

      找出总和为0 的子集。

      算法是众所周知的。这是一个基于Wikipedia explanations的Python实现

      class Q:
          def __init__(self, values):
              self.len = len(values)
              self.min = sum(e for e in values if e <= 0)
              self.max = sum(e for e in values if e >= 0)
              self._arr = [False] * self.len * (self.max - self.min + 1)
      
          def __getitem__(self, item):
              index, v = item
              return self._arr[v * self.len + index]
      
          def __setitem__(self, item, value):
              index, v = item
              self._arr[v * self.len + index] = value
      
      
      class SubsetSum:
          def __init__(self, values):
              self._values = values
              self._q = Q(values)
      
          def prepare(self):
              for s in range(self._q.min, self._q.max + 1):
                  self._q[0, s] = (self._values[0] == s)
              for i in range(self._q.len):
                  self._q[i, 0] = True
      
              for i in range(1, self._q.len):
                  v = self._values[i]
                  for s in range(self._q.min, self._q.max + 1):
                      self._q[i, s] = (v == s) or self._q[i - 1, s] or self._q[
                          i - 1, s - v]
      
          def subsets(self, target=0):
              yield from self._subsets(self._q.len - 1, target, [])
      
          def _subsets(self, i, target, p):
              assert i >= 0
              v = self._values[i]
              c = self._q[i - 1, target]
              b = self._q[i - 1, target - v]
              if i == 0:
                  if target == 0:
                      if p:
                          yield p
                  elif self._q[0, target]:
                      yield p + [i]
              else:
                  if self._q.min <= target - v <= self._q.max and self._q[
                      i - 1, target - v]:
                      yield from self._subsets(i - 1, target - v, p + [i])
      
                  if self._q[i - 1, target]:
                      yield from self._subsets(i - 1, target, p)
      
      

      它是这样工作的:

      arr = [-10, 1, 2, 20, 5, -100, -80, 10, 15, 15, 60, 100, -20, -18]
      arr = sorted(arr)
      s = SubsetSum(arr)
      s.prepare()
      subsets0 = list(s.subsets())
      print(subsets0)
      

      输出:

      [[13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], [13, 12, 11, 10, 9, 7, 6, 5, 3, 2, 1, 0], [13, 12, 11, 10, 9, 4, 2, 1, 0], [13, 12, 11, 10, 8, 7, 4, 2, 1, 0], [13, 12, 11, 10, 8, 6, 5, 4, 3, 1, 0], [13, 12, 11, 10, 7, 2, 1, 0], [13, 12, 11, 10, 6, 5, 3, 1, 0], [13, 12, 11, 9, 8, 7, 4, 2, 1, 0], [13, 12, 11, 9, 8, 6, 5, 4, 3, 1, 0], [13, 12, 11, 9, 7, 2, 1, 0], [13, 12, 11, 9, 6, 5, 3, 1, 0], [13, 12, 11, 8, 7, 6, 5, 3, 1, 0], [13, 12, 11, 8, 4, 1, 0], [13, 12, 11, 1, 0], [13, 12, 10, 9, 8, 7, 6, 5, 4, 3, 1, 0], [13, 12, 10, 9, 8, 2, 1, 0], [13, 12, 10, 9, 7, 6, 5, 3, 1, 0], [13, 12, 10, 9, 4, 1, 0], [13, 12, 10, 8, 7, 4, 1, 0], [13, 12, 10, 7, 1, 0], [13, 12, 9, 8, 7, 4, 1, 0], [13, 12, 9, 7, 1, 0], [13, 11, 10, 8, 6, 5, 4, 3, 2, 0], [13, 11, 10, 6, 5, 3, 2, 0], [13, 11, 9, 8, 6, 5, 4, 3, 2, 0], [13, 11, 9, 6, 5, 3, 2, 0], [13, 11, 8, 7, 6, 5, 3, 2, 0], [13, 11, 8, 4, 2, 0], [13, 11, 7, 6, 5, 4, 3, 2, 1], [13, 11, 7, 6, 5, 4, 3, 0], [13, 11, 2, 0], [13, 10, 9, 8, 7, 6, 5, 4, 3, 2, 0], [13, 10, 9, 7, 6, 5, 3, 2, 0], [13, 10, 9, 4, 2, 0], [13, 10, 8, 7, 4, 2, 0], [13, 10, 8, 6, 5, 4, 3, 2, 1], [13, 10, 8, 6, 5, 4, 3, 0], [13, 10, 7, 2, 0], [13, 10, 6, 5, 3, 2, 1], [13, 10, 6, 5, 3, 0], [13, 9, 8, 7, 4, 2, 0], [13, 9, 8, 6, 5, 4, 3, 2, 1], [13, 9, 8, 6, 5, 4, 3, 0], [13, 9, 7, 2, 0], [13, 9, 6, 5, 3, 2, 1], [13, 9, 6, 5, 3, 0], [13, 8, 7, 6, 5, 3, 2, 1], [13, 8, 7, 6, 5, 3, 0], [13, 8, 4, 2, 1], [13, 8, 4, 0], [13, 7, 6, 5, 4, 3, 1], [13, 2, 1], [13, 0], [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 0], [12, 11, 10, 9, 8, 2, 0], [12, 11, 10, 9, 7, 6, 5, 3, 2, 1], [12, 11, 10, 9, 7, 6, 5, 3, 0], [12, 11, 10, 9, 4, 2, 1], [12, 11, 10, 9, 4, 0], [12, 11, 10, 8, 7, 4, 2, 1], [12, 11, 10, 8, 7, 4, 0], [12, 11, 10, 8, 6, 5, 4, 3, 1], [12, 11, 10, 7, 2, 1], [12, 11, 10, 7, 0], [12, 11, 10, 6, 5, 3, 1], [12, 11, 9, 8, 7, 4, 2, 1], [12, 11, 9, 8, 7, 4, 0], [12, 11, 9, 8, 6, 5, 4, 3, 1], [12, 11, 9, 7, 2, 1], [12, 11, 9, 7, 0], [12, 11, 9, 6, 5, 3, 1], [12, 11, 8, 7, 6, 5, 3, 1], [12, 11, 8, 4, 1], [12, 11, 1], [12, 10, 9, 8, 7, 6, 5, 4, 3, 1], [12, 10, 9, 8, 2, 1], [12, 10, 9, 8, 0], [12, 10, 9, 7, 6, 5, 3, 1], [12, 10, 9, 4, 1], [12, 10, 8, 7, 4, 1], [12, 10, 7, 1], [12, 9, 8, 7, 4, 1], [12, 9, 7, 1], [11, 10, 8, 6, 5, 4, 3, 2], [11, 10, 6, 5, 3, 2], [11, 9, 8, 6, 5, 4, 3, 2], [11, 9, 6, 5, 3, 2], [11, 8, 7, 6, 5, 3, 2], [11, 8, 4, 2], [11, 7, 6, 5, 4, 3], [11, 2], [10, 9, 8, 7, 6, 5, 4, 3, 2], [10, 9, 7, 6, 5, 3, 2], [10, 9, 4, 2], [10, 8, 7, 4, 2], [10, 8, 6, 5, 4, 3], [10, 7, 2], [10, 6, 5, 3], [9, 8, 7, 4, 2], [9, 8, 6, 5, 4, 3], [9, 7, 2], [9, 6, 5, 3], [8, 7, 6, 5, 3], [8, 4]]
      

      减少子集的数量

      我们有 105 个子集的总和为 0,但我们可以删除作为其他子集超集的子集。我们需要一个函数来查找元素列表是否包含另一个列表中的所有元素。在 Python 中:

      import collections
      
      def contains(l1, l2):
          """
          Does l1 contain all elements of l2?
          """
          c = collections.Counter(l1)
          for e in l2:
              c[e] -= 1
          return all(n >= 0 for n in c.values())
      

      现在,我们可以删除作为另一个子集的超集的子集。

      def remove_supersets(subsets):
          subsets = sorted(subsets, key=len)
          new_subsets = []
          for i, s1 in enumerate(subsets):
              for s2 in subsets[:i]: # smaller subsets
                  if contains(s1, s2):
                      break
              else:  # not a superset
                  new_subsets.append(s1)
          return new_subsets
      

      在我们的情况下:

      subsets0 = remove_supersets(subsets0)
      print(len(subsets0))
      

      输出:

      [[13, 0], [11, 2], [8, 4], [13, 2, 1], [12, 11, 1], [10, 7, 2], [9, 7, 2], [12, 10, 7, 1], [12, 9, 7, 1], [10, 9, 4, 2], [10, 6, 5, 3], [9, 6, 5, 3], [12, 11, 10, 7, 0], [12, 11, 9, 7, 0], [12, 10, 9, 8, 0], [12, 10, 9, 4, 1], [8, 7, 6, 5, 3], [12, 11, 10, 9, 4, 0], [12, 10, 9, 8, 2, 1], [11, 7, 6, 5, 4, 3], [13, 7, 6, 5, 4, 3, 1]]
      [[0, 2, 10, 6, 4], [0, 2, 10, 8, 1], [0, 2, 11, 5, 4], [0, 2, 11, 7, 1], [0, 16, 9, 4], [0, 16, 15, 1], [0, 18, 19], [3, 2, 12, 11], [3, 2, 13, 10], [3, 17, 16], [3, 19, 14], [20, 14, 1]]
      

      我们设法将子集的数量减少到 21 个,这是一个很好的改进,因为我们需要探索所有可能性来找到准确的覆盖。

      算法 X

      我不在这里使用跳舞链接(我认为我们将为 C 等低级语言设计该技术,但如果您愿意,您可以在 Python 中实现它们)。我们只需要跟踪剩余的子集:

      class Matrix:
          def __init__(self, subsets, ignore_indices=set()):
              self._subsets = subsets
              self._ignore_indices = ignore_indices
      
          def subset_values(self, i):
              assert i not in self._ignore_indices
              return self._subsets[i]
      
          def value_subsets_indices(self, j):
              return [i for i, s in self._subsets_generator() if j in s]
      
          def _subsets_generator(self):
              return ((i, s) for i, s in enumerate(self._subsets) if
                      i not in self._ignore_indices)
      
          def rarest_value(self):
              c = collections.Counter(
                  j for _, s in self._subsets_generator() for j in s)
              return c.most_common()[-1][0]
      
          def take_subset(self, i):
              s = self._subsets[i]
              to_ignore = {i2 for i2, s2 in self._subsets_generator() if
                           set(s2) & set(s)}
              return Matrix(self._subsets,
                            self._ignore_indices | to_ignore)
      
          def __bool__(self):
              return bool(list(self._subsets_generator()))
      

      最后是cover 函数:

      def cover(m, t=[]):
          if m: # m is not empty
              j = m.rarest_value()
              for i in m.value_subsets_indices(j):
                  m2 = m.take_subset(i)
                  yield from cover(m2, t + [i])
          else:
              yield t
      

      最后,我们有:

      m = Matrix(subsets0)
      ts = list(cover(m))
      t = max(ts, key=len)
      print([[arr[j] for j in subsets0[i]] for i in t])
      

      输出:

      [[100, -100], [10, -10], [15, 2, 1, -18], [15, 5, -20], [60, 20, -80]]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-01-21
        • 2022-06-25
        • 1970-01-01
        • 1970-01-01
        • 2021-12-09
        • 2012-03-21
        • 2019-07-10
        • 2021-11-04
        相关资源
        最近更新 更多