【问题标题】:Given an array of integers, create partitions where the sum of elements in every partition is 0 and maximum no of partitions are formed给定一个整数数组,创建分区,其中每个分区中的元素总和为 0,并且形成的最大分区数
【发布时间】:2020-02-23 20:21:50
【问题描述】:

我的规则:

  • 允许重复
  • 显然允许负数
  • 由于我提到了分区,这意味着您不能将数组中的元素放在超过 1 个分区中
    • 分区中的元素是子集/不必是连续的数组块
    • 输入数组中的元素没有排序
    • 输入数组中所有元素的总和将为 0(给定条件)

示例:如果 A = {-2,-4,-6,+6,+6} 那么 B={{-2,-4,6},{+6,-6}} 的结果是最大分区数

仅供参考,我想返回所有分区而不是分区数。

根据我的研究,这似乎是一个 NP 难/完全问题。但我不确定,如果有人能解释最好的解决方法(即使它很慢),我将不胜感激。一些伪代码也将不胜感激。

谢谢。

【问题讨论】:

  • 分区应该是连续的数组块还是子集?
  • @MBo 它们是子集

标签: algorithm math np subset-sum partition-problem


【解决方案1】:

这绝对有一种 NP-hard 的感觉。特别是是否可以进行 2 个分区与 1 个分区的问题是一个问题,即除最后一个之外的所有元素的适当子集是否加起来是最后一个的负数,这是子集和问题的一个变体。因此,即使通过拆分其中一个分区来验证无法进一步改进答案也可能是 NP 完全的!

但是在实践中如何解决这个问题?

第 1 步是生成一个数据结构,表示所有可能的分区,包括总和为 0 的第一个元素。这可以像标准子集总和算法一样解决。有了一个双向链表的想法,我们可以从中获取信息。 (双向链表通常与您的数组大小乘以找到的不同和的数量成正比,但在解码时,它可能会产生指数级的分区数。

双向链表会让你头晕目眩,所以你可能想使用如下语法糖:

from collections import namedtuple
Node = namedtuple('Node', ['ind', 'prev'])
Paths = namedtuple('Paths', ['node', 'prev'])

ind 是数组的索引,node 始终是 Nodeprev 始终是 PathsNone

现在Node 表示“在以下先前选择中包含此索引和任何有效路径”。而Paths 则表示“这是一个通向这里的节点,还有一条通往其他方式的先前路径。”

有了这个,我们可以做到以下几点:

# We only want paths that take the 0th index.
ways_to_get_to_n = {elements[0], Paths(Node(0, None), None)}

for i in range(1, len(elements)):
    element = elements[i]
    new_ways_to_get_n = {}
    for value, ways in ways_to_get_n.items():
        new_ways_to_get_n[value] = ways
    for value, ways in ways_to_get_n.items():
        if value + element not in new_ways_to_get_n:
            new_ways_to_get_n[value + element] = Paths(Node(i, ways), None)
        else:
            new_ways_to_get_n[value + element] = Paths(Node(i, ways), new_ways_to_get_n[value + element])
    ways_to_get_n = new_ways_to_get_n

完成后,ways_to_get_n[0] 是一个相当简洁的数据结构,可以使用双递归遍历包含第一个元素的所有分区。然而,有一个挑战。这些分区内部可能有 0 个分区。因此,当您沿着双递归进行时,请携带您可以达到的所有值的数据结构(相同的旧子集求和技巧),并在出现 0 时提前终止。 (这种记账可能感觉像是很多额外的工作,但它实际上会为您节省更多。)

现在您可以递归地找到第一个元素的最小分区。然后递归寻找如何划分剩余元素。每次你这样做时,你都会与你目前拥有的最好的进行比较,如果是改进,请记录下来。

当你完成了所有分解它的方法时,你就完成了。

假设整数数组(因此子集和伪多项式可能对您有利),这将使您能够相当有效地在最小分区上进行递归。与幼稚的方法相比,这是一种更好的递归爆炸。但它会比你想象的要大很多

我怀疑第一步以递增的绝对值对数组进行排序将使该算法更有效,因为当您仍然有很多元素时,“不可约划分的过滤器”可能会提前脱离递归。

【讨论】:

  • 这是另一个很好的攻击和话语。请注意,该问题并未说明输入列表将被用尽:不能保证任何特定整数都将包含在最佳解决方案或任何解决方案中。我没有更正,只是强调一个可能无效的实现假设。
  • @Prune 实际上有保证。原始数组的总和为 0 的声明意味着将所有不属于建议答案的整数放入一个额外的分区中可以改进该答案。因此最优解必须包含所有整数。
  • 啊,对了!别介意评论,期待第一句话。 :-)
  • 不错的解决方案。但我不确定这是否是最快的方法。我看到了另一个关于使用 Bin 打包和分区问题的帖子关于如何使用这些问题有什么想法吗?
【解决方案2】:

我同意问题是 NP。优化这个是丑陋的,大写的“呃”。您可以稍微缩短搜索时间,但我担心它仍然是 O(2^N) 甚至更糟。

  • 将列表分为正数和负数;两者都排序 列表。
  • 对于较短列表中的每个元素,在另一个元素中寻找它的补码。每个这样的对都是一个分区;这些可以安全地放在解决方案列表中,并从进一步处理中删除。

现在是丑陋的部分。 “相对”的蛮力方法是生成每个分区的幂集;按总和索引。例如,给定列表2, 3, 4, 7

 2  [2]
 3  [3]
 4  [4]
 5  [2, 3]
 6  [2, 4]
 7  [7] [3, 4]
 9  [2, 7] [2, 3, 4]
10  [3, 7]
11  [4, 7]
12  [2, 3, 7]
13  [2, 4, 7]
14  [3, 4, 7]
16  [2, 3, 4, 7]

现在在正面和负面清单之间找到 abs(sum(subset)) 的所有匹配项。这些构成了您的解决方案空间的选择。从这一点来看,我最好的建议是将set coverage problem 应用于此;您必须稍微调整一下重复值。

【讨论】:

  • 快速简单的分区是一种贪心算法。它会很快得到好的解决方案,但不能保证最好的分区。是的,“最好”与“好”让这个问题变得更加困难!
  • 我已删除 only 对表单 (i, -i)。我相信这对于找到最佳解决方案是安全的。
  • 啊。我不确定,但不能轻易提出反例。
猜你喜欢
  • 1970-01-01
  • 2013-09-01
  • 1970-01-01
  • 2020-06-03
  • 2017-02-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多