如何将整数列表分成两组整数,其总和差异最小?

How to split a list of integers into two groups of integers with minimal difference of their sums?

提问人:Ξένη Γήινος 提问时间:10/18/2023 更新时间:10/19/2023 访问量:166

问:

我不知道这是否是重复的,但谷歌搜索像往常一样返回任何相关内容。

所以我有一个整数列表,它表示段的长度。我想将整数列表拆分为两个整数列表,这样组长度之和之间的差异就很小。

这是关于我的 GUI 程序:

enter image description here

程序完全是我自己创建的,程序快要完成了,完成后我会把它发布到 Code Review。我分别开发了每个部分,现在我正在努力改进布局,以尽量减少窗口内未使用的区域。

这是一个矩形包装问题,但由于零件的高度相等,因此将其简化为生产线包装问题。我只需要将它们分成两行,并尽量减少行的宽度差异。

顺便说一句,GUI 的许多部分都由线程控制,如果触发,它们会动画化。这是我写的两个 AI 之间的实时井字游戏。你可以在这里看到我的程序在运行。

所以我试图解决这个问题,我确实找到了一个解决方案,但这个解决方案效率极低,只需要 282240 次迭代才能找到 8 个项目的解决方案。

我的数学不是很好,所以我就选择了最直接的方法,我只是生成序列的每一个排列,然后通过索引切片将每个排列拆分成每一对可能的子列表,然后计算出绝对差值并将对和对应的差值存储在字典中,然后找到最小值。

from itertools import permutations

lengths = [240, 255, 270, 240, 220, 230, 420, 470]

items = {}

for perm in permutations(lengths):
    for i in range(1, 8):
        a = perm[:i]
        b = perm[i:]
        items[(a, b)] = abs(sum(a) - sum(b))

min(items.items(), key=lambda x: x[1])

我明白了:

(((240, 270, 240, 420), (255, 220, 230, 470)), 5)

有没有更有效的方法可以做到这一点?

Python 算法 数学

评论

1赞 Scott Hunter 10/18/2023
你能利用这样一个事实,即你希望每一半的总和尽可能接近整个事情的总和的一半吗?
0赞 Prasanna 10/18/2023
这是一回事吗?wentao-shao.gitbook.io/leetcode/array/1231.divide-chocolate
0赞 Ξένη Γήινος 10/18/2023
@Prasanna 好像不是,我看过了。
1赞 Simon Goater 10/18/2023
“我确实找到了一个解决方案,但该解决方案效率极低,只需 282240 次迭代才能找到 8 个项目的解决方案”您可以在 2^8 = 256 次迭代中“蛮力”它,所以我不知道你在做什么。
0赞 Christoph Rackwitz 10/21/2023
我投票决定关闭这个问题,因为我相信这个问题在计算机科学或理论计算机科学的堆栈交换中问得更好。问题不在于编程,而在于逻辑/理论/算法。

答:

2赞 Dillon Davis 10/18/2023 #1

假设您只有两行,如图所示,您的问题可以被框定为分区问题的优化变体 - 获取多组整数(在您的情况下为宽度)并将它们划分为两个子集的问题,其中它们的元素总和(组合宽度)的差异最小化。

有一些近似算法,也有一些精确的算法。问题是 NP 困难的,但考虑到您上面显示的元素数量,问题大小应该足够小,不会成为问题。如果你想要一个非常精确/精确的解决方案,或者想要一个随时算法,我建议你使用完整的Karmarkar-Karp算法。

在您的特定情况下,我可能会选择伪多项式时间数划分算法。你没有很多元素,所以这部分不会导致任何内存问题,只有数字的大小(元素宽度)可能会有问题。如果数字太大,我会将它们四舍五入到最接近的 X 像素,然后将所有宽度除以 X,以获得更小的数字(以牺牲一定的准确性为代价,除非您实际调整组件大小以真正成为 X 像素宽度的倍数)。

最后一件事:如果你想在元素之间填充,你也需要在宽度中包含它。

2赞 btilly 10/18/2023 #2

这是一个使用动态编程的简单解决方案。

def split (lengths):
    path = {0: [-1]}
    target = sum(lengths) // 2
    for i in range(len(lengths)):
        next_path = path.copy()
        value = lengths[i]
        for prev_value, prev_path in path.items():
            next_value = value + prev_value
            if next_value <= target and next_value not in next_path:
                next_path[value + prev_value] = [i, prev_path]
        path = next_path

    group_a = []
    group_b = []
    path_data = path[max(path.keys())]
    for i in range(len(lengths) - 1, -1, -1):
        if i == path_data[0]:
            group_a.append(lengths[i])
            path_data = path_data[1]
        else:
            group_b.append(lengths[i])

    return (list(reversed(group_a)), list(reversed(group_b)), sum(group_b) - sum(group_a))


lengths = [240, 255, 270, 240, 220, 230, 420, 470]
print(split(lengths))

评论

1赞 Ξένη Γήινος 10/18/2023
您的代码不适用于所有输入。无论出于何种原因,我测试了一个包含 16 个元素的样本,例如: ,并以 that 作为参数调用 ,这是因为: .事实上,我测试的每 16 个元素样本都会导致此异常。[5637, 50322, 44890, 11103, 59401, 28521, 19599, 22584, 9960, 30408, 42024, 14913, 25560, 60242, 350, 53873]splitTypeError: 'int' object is not subscriptableif i == path_data[0]:
0赞 btilly 10/19/2023
@ΞένηΓήινος哎呀。我将路径初始化为而不是。当第一个值不在较小的组中时,这种情况会中断。现已修复。对于您的示例,在大约 1/100 秒内找到。{0: -1}{0: [-1]}([44890, 28521, 19599, 9960, 42024, 14913, 25560, 350, 53873], [5637, 50322, 11103, 59401, 22584, 30408, 60242], 7)
0赞 Ξένη Γήινος 10/19/2023
我已经彻底测试了您的方法,对于 16 个元素,我使用组合的新方法需要 77 毫秒才能找到最佳解决方案,而您的代码需要 16 毫秒才能找到它。我还验证了您的方法确实产生了正确的结果。所以我会接受它。
0赞 Ξένη Γήινος 10/18/2023 #3

当我发布这个问题时,我没有多想,我绝对没有尽力而为。我只是尝试了一种蹩脚的方法来问这个问题,因为我知道在不到 2 次n-1 次迭代中很难找到 n 个元素的确切解决方案。

但现在我又试了一次,找到了两个更好的解决方案。一种是我发现的获得精确解决方案的最有效方法,另一种是找到良好近似值的非常有效的方法。

                                       1

                                    1    1

                                  1    2    1

                               1    3    3    1

                             1    4    6    4    1

                          1    5    10   10   5    1

                        1    6    15   20   15   6    1

                     1    7    21   35   35   21   7    1

                   1    8    28   56   70   56   28   8    1

                1    9    36   84  126  126   84   36   9    1

              1    10   45  120  210  252  210  120   45   10   1

           1    11   55  165  330  462  462  330  165   55   11   1

         1    12   66  220  495  792  924  792  495  220   66   12   1

      1    13   78  286  715  1287 1716 1716 1287 715  286   78   13   1

    1    14   91  364  1001 2002 3003 3432 3003 2002 1001 364   91   14   1

 1    15  105  455  1365 3003 5005 6435 6435 5005 3003 1365 455  105   15   1

以上是帕斯卡三角形。每个数字表示从 r 的总体中选择 c 元素而不重复的方法总数,其中 c 是数字所在列的索引,r 是数字所在的行索引,索引从 0 开始。

从 n 个元素中进行选择的总可能方式是 2n

现在正如你所看到的,三角形是对称的,组是互斥的,这意味着如果你选择了一个元素,你会自动知道其他n个元素——一个没有选择的元素。因此,我只需要生成长度为 i 的样本的所有可能组合,其中 1 <= i <= n // 2,然后找到总和差最小的对。

此方法需要帕斯卡三角形迭代的第 n 行的一半才能找到 n 个元素的解。

该系列是:

In [2734]: [sum(row[:i // 2 + i % 2]) for i, row in enumerate(pt.data, start=1)]
Out[2734]: [1, 1, 3, 4, 11, 16, 42, 64, 163, 256, 638, 1024, 2510, 4096, 9908, 16384]

我没有仔细看过它,但这个系列或多或少是 2n - 1

另一种方法要简单得多,它在 O(log n + n) 时间内找到一个解决方案,但解决方案并不是最好的。第一步是以相反的顺序对元素进行排序,以便较大的数字列在较小的数字之前。

这是为了最大程度地减少后面数字的影响,因此即使路径不是最佳路径,它们也不会对两个列表之间的差异产生太大影响。

下一部分很简单,做两个空列表。然后,对于每个数字,计算一个列表的总和加上另一个列表的数字和之和之间的绝对差。并将数字附加到列表中,以产生最小的差异。

此解决方案在每个交汇点做出最优决策,但未找到全局最优。

from itertools import chain, combinations

def even_bisect(s):
    s = list(s)
    total = sum(s)
    choices = chain.from_iterable(combinations(s, i) for i in range(1, len(s) // 2 + 1))
    triples = []
    for choice in choices:
        other = s.copy()
        for e in choice:
            other.remove(e)

        triples.append((choice, tuple(other), abs(2 * sum(choice) - total)))

    return min(triples, key=lambda x: x[2])


def even_partition(numbers):
    assert len(numbers) >= 2
    numbers = sorted(numbers, reverse=True)
    l1 = []
    l2 = []
    s1 = 0
    s2 = 0
    for n in numbers:
        d1 = abs(s1 + n - s2)
        d2 = abs(s2 + n - s1)
        if d1 <= d2:
            l1.append(n)
            s1 += n
        else:
            l2.append(n)
            s2 += n

    return l1, l2
In [2738]: lengths = [240, 255, 270, 240, 220, 230, 420, 470]

In [2739]: %timeit even_partition(lengths)
4.68 µs ± 100 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

In [2740]: %timeit even_bisect(lengths)
167 µs ± 1.72 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

In [2741]: even_bisect(lengths)
Out[2741]: ((240, 270, 240, 420), (255, 220, 230, 470), 5)

In [2742]: a, b = even_partition(lengths)

In [2743]: a, b, abs(sum(a) - sum(b))
Out[2743]: ([470, 255, 240, 220], [420, 270, 240, 230], 25)