将数字列表划分为2个相等总和列表的算法

Posted

技术标签:

【中文标题】将数字列表划分为2个相等总和列表的算法【英文标题】:Algorithm to Divide a list of numbers into 2 equal sum lists 【发布时间】:2010-10-27 18:31:20 【问题描述】:

有一个数字列表。 该列表将被分成 2 个大小相等的列表,总和的差异最小。总和必须打印出来。

#Example:
>>>que = [2,3,10,5,8,9,7,3,5,2]
>>>make_teams(que)
27 27

以下代码算法在某些情况下是否有错误?

如何优化和/或 Python 化它?

def make_teams(que):
    que.sort()
    if len(que)%2: que.insert(0,0)
    t1,t2 = [],[]
    while que:
        val = (que.pop(), que.pop())
        if sum(t1)>sum(t2):
            t2.append(val[0])
            t1.append(val[1])
        else:
            t1.append(val[0])
            t2.append(val[1])
    print min(sum(t1),sum(t2)), max(sum(t1),sum(t2)), "\n"

问题来自http://www.codechef.com/problems/TEAMSEL/

【问题讨论】:

这是装箱问题的变种吗?这是一个 NP 完全问题,IIRC。 que = [1,50,50,100] 应该给你 100 和 101 的团队。我认为你的算法会产生 51 和 150。 @S.Lott 这是一个编程竞赛中的练习题。这里是参考:codechef.com/problems/TEAMSEL我最好的理解说,没错。但系统将其标记为错误。 @Alex B:当我运行它时,我得到了 100 和 101。 @Alex B:我得到 100 和 101 是对的,供您参考。 【参考方案1】:

好吧,您可以在多项式时间内找到百分比精度的解,但要真正找到最优(绝对最小差)解,问题是 NP 完全的。这意味着该问题没有多项式时间解。因此,即使数字列表相对较少,计算量也太大而无法解决。如果您确实需要解决方案,请查看一些用于此的近似算法。

http://en.wikipedia.org/wiki/Subset_sum_problem

【讨论】:

这是错误的。这是背包问题,可以用动态规划来解决。 我认为这不是子集总和问题......尽管我会坦率地承认,我离开这个领域太久了,无法保证这样说。我喜欢 GS 概述的动态编程方法。你能解释一下为什么这行不通吗? @gs:没错。您可以将其视为子集和问题或背包问题(从技术上讲,它称为数字分区问题),因为无论如何所有 NP 完全问题都是等价的。 :-) 这个问题说明了为什么不要被“NP-complete”这个术语冲昏了头脑:没有已知算法在 输入大小 中是多项式的(输入中的位数) 在最坏的情况下,但正如动态规划算法所示,它可以在输入 numbers 本身中按时间多项式完成。背包也是如此:查找“伪多项式时间”。【参考方案2】:
class Team(object):
    def __init__(self):
        self.members = []
        self.total = 0

    def add(self, m):
        self.members.append(m)
        self.total += m

    def __cmp__(self, other):
        return cmp(self.total, other.total)


def make_teams(ns):
    ns.sort(reverse = True)
    t1, t2 = Team(), Team()

    for n in ns:
        t = t1 if t1 < t2 else t2
        t.add(n)

    return t1, t2

if __name__ == "__main__":
    import sys
    t1, t2 = make_teams([int(s) for s in sys.argv[1:]])
    print t1.members, sum(t1.members)
    print t2.members, sum(t2.members)

>python two_piles.py 1 50 50 100
[50, 50] 100
[100, 1] 101

【讨论】:

这对我来说太复杂了。【参考方案3】:

Dynamic programming 是您正在寻找的解决方案。

以 [4, 3, 10, 3, 2, 5] 为例:

X 轴:组的可达总和。 max = sum(所有数字)/ 2(四舍五入) Y 轴:对组中的元素进行计数。最大值 = 计数 / 2(四舍五入) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 | | | | 4| | | | | | | | | | | // 4 2 | | | | | | | | | | | | | | | 3 | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 | | | 3| 4| | | | | | | | | | | // 3 2 | | | | | | | 3| | | | | | | | 3 | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 | | | 3| 4| | | | | |10| | | | | // 10 2 | | | | | | | 3| | | | | |10|10| 3 | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 | | | 3| 4| | | | | |10| | | | | // 3 2 | | | | | | 3| 3| | | | | |10|10| 3 | | | | | | | | | | 3| | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 | | 2| 3| 4| | | | | |10| | | | | // 2 2 | | | | | 2| 3| 3| | | | | 2|10|10| 3 | | | | | | | | 2| 2| 3| | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 | | 2| 3| 4| 5| | | | |10| | | | | // 5 2 | | | | | 2| 3| 3| 5| 5| | | 2|10|10| 3 | | | | | | | | 2| 2| 3| 5| 5| | | ^

12是我们的幸运数字!回溯获取组:

12 - 5 = 7 5 7 - 3 = 4 5, 3 4 - 4 = 0 5, 3, 4

然后可以计算另一组:4,3,10,3,2,5 - 5,3,4 = 10,3,2

所有带数字的字段都是一个袋子的可能解决方案。选择右下角最远的那个。

顺便说一句:它被称为knapsack-problem。

如果所有权重(w1,...,wn 和 W)都是 非负整数,背包 问题可以解决 使用动态的伪多项式时间 编程。

【讨论】:

好的。这对我来说很有意义......但是为什么人们声称这个问题是NP完全的?有人错了……我不知道这个解决方案有什么问题(如果有的话)。 你需要一个 O(Sum(x[i]) 的空间才能使用动态规划解决方案。总的来说,我相信问题是 NP-Complete。(想想如果每个数字是浮点数,不能轻易使用动态规划) 没错,它只适用于问题的有限子集。 伪多项式时间 (en.wikipedia.org/wiki/Pseudo-polynomial_time) 表示时间在输入数字的大小上是多项式的,但在输入的长度上仍然是非多项式的。如果输入数字大小是有界的,那么您有一个多项式时间算法。但如果它是无限的,那就不是。例如,如果背包的 n 个输入数字是 2^0、2^1、...、2^(n-1),那么在动态规划解的最后一步中,您有 2^n 个解要检查。 因为它本质上是正确的:有一个有效的动态规划算法。 (您只需要为可能的[nitems][sum] 保留布尔值,而不仅仅是为每个总和保留一个布尔值。)【参考方案4】:

为了提高性能,您可以通过将 append() 和 sum() 替换为运行总计来节省计算量。

【讨论】:

对我来说听起来像是过早的优化。【参考方案5】:

您可以使用以下方法稍微收紧循环:

def make_teams(que):
    que.sort()
    t1, t2 = []
    while que:
        t1.append(que.pop())
        if sum(t1) > sum(t2):
            t2, t1 = t1, t2
    print min(sum(t1),sum(t2)), max(sum(t1),sum(t2))

【讨论】:

【参考方案6】:

请注意,它也是一种启发式方法,我将排序从函数中移出。

 def g(data):
   sums = [0, 0]
   for pair in zip(data[::2], data[1::2]):
     item1, item2 = sorted(pair)
     sums = sorted([sums[0] + item2, sums[1] + item1])
   print sums

data = sorted([2,3,10,5,8,9,7,3,5,2])
g(data)

【讨论】:

【参考方案7】:

你的方法不起作用的测试用例是

que = [1, 1, 50, 50, 50, 1000]

问题是您要成对分析事物,在此示例中,您希望所有 50 人都在同一个组中。但是,如果您删除配对分析方面并且一次只输入一个条目,则应该可以解决此问题。

这是执行此操作的代码

def make_teams(que):
    que.sort()
    que.reverse()
    if len(que)%2: que.insert(0,0)
    t1,t2 = [],[]
    while que:
        if abs(len(t1)-len(t2))>=len(que):
            [t1, t2][len(t1)>len(t2)].append(que.pop(0))
        else:
            [t1, t2][sum(t1)>sum(t2)].append(que.pop(0))
    print min(sum(t1),sum(t2)), max(sum(t1),sum(t2)), "\n"

if __name__=="__main__":
    que = [2,3,10,5,8,9,7,3,5,2]
    make_teams(que)
    que = [1, 1, 50, 50, 50, 1000]
    make_teams(que)

这给出了 27、27 和 150、1002,这对我来说是有意义的答案。

编辑:在审查中,我发现这实际上不起作用,但最后,我不太清楚为什么。不过,我会在这里发布我的测试代码,因为它可能有用。该测试只是生成具有相等总和的随机序列,将它们放在一起并进行比较(结果很糟糕)。

编辑#2:根据 Unknown [87,100,28,67,68,41,67,1] 指出的示例,很明显为什么我的方法不起作用。具体来说,为了解决这个例子,需要将两个最大的数字都添加到同一个序列中以获得有效的解决方案。

def make_sequence():
    """return the sums and the sequence that's devided to make this sum"""
    while 1:
        seq_len = randint(5, 200)
        seq_max = [5, 10, 100, 1000, 1000000][randint(0,4)]
        seqs = [[], []]
        for i in range(seq_len):
            for j in (0, 1):
                seqs[j].append(randint(1, seq_max))
        diff = sum(seqs[0])-sum(seqs[1])
        if abs(diff)>=seq_max: 
            continue
        if diff<0:
            seqs[0][-1] += -diff
        else:
            seqs[1][-1] += diff
        return sum(seqs[0]), sum(seqs[1]), seqs[0], seqs[1]

if __name__=="__main__":

    for i in range(10):
        s0, s1, seq0, seq1 = make_sequence()
        t0, t1 = make_teams(seq0+seq1)
        print s0, s1, t0, t1
        if s0 != t0 or s1 != t1:
            print "FAILURE", s0, s1, t0, t1

【讨论】:

您提供了一个测试用例来证明这一点是错误的。赞成。我成对做的原因是因为两个列表中的条目数量必须相等。 是的,但我认为任何简单的解决方案都是启发式的,最好的结果应该是 1002 150。 @odwl:我同意你的观点。当您成对执行此操作时,您将得到 101、1051,而逐项给出 150、1002。 @becomingGuru,我实现了一个正常工作的解决方案,看看它。 @tom10 实际上,您的解决方案对于 [87,100,28,67,68,41,67,1] 失败。它输出 223 236。不错的尝试。【参考方案8】:

由于列表必须等于我,所以问题根本不是 NP。

我用模式 t1

def make_teams2(que):
    que.sort()
    if len(que)%2: que.insert(0,0)
    t1 = []
    t2 = []
    while que:
        if len(que) > 2:
            t1.append(que.pop(0))
            t1.append(que.pop())
            t2.append(que.pop(0))
            t2.append(que.pop())
        else:
            t1.append(que.pop(0))
            t2.append(que.pop())
    print sum(t1), sum(t2), "\n"

编辑:我想这也是一个错误的方法。结果错误!

【讨论】:

我可以重构它,但它无论如何都不起作用。算法很简单,我的代码很糟糕:) 列表不必完全相等。也可以有 4 人团队和 5 人团队。看看我的解决方案是否可行。【参考方案9】:

实际上是 PARTITION,KNAPSACK 的一个特例。

它是 NP Complete,具有伪多项式 dp 算法。伪多项式中的伪是指运行时间取决于权重的范围。

一般而言,您必须先确定是否存在精确解决方案,然后才能接受启发式解决方案。

【讨论】:

【参考方案10】:

经过一番思考,对于不太大的问题,我认为最好的启发式方法是:

import random
def f(data, nb_iter=20):
  diff = None
  sums = (None, None)
  for _ in xrange(nb_iter):
    random.shuffle(data)
    mid = len(data)/2
    sum1 = sum(data[:mid])
    sum2 = sum(data[mid:])
    if diff is None or abs(sum1 - sum2) < diff:
      sums = (sum1, sum2)
  print sums

如果问题较大,可以调整 nb_iter。

它几乎可以解决上面提到的所有问题。

【讨论】:

查看我的答案以获得有保证的确定性解决方案【参考方案11】:

新解决方案

这是一种带有启发式剔除的广度优先搜索。这棵树仅限于玩家/2 的深度。玩家总分限制为总分/2。玩家池为 100 人,大约需要 10 秒才能解决。

def team(t):
    iterations = range(2, len(t)/2+1)

    totalscore = sum(t)
    halftotalscore = totalscore/2.0

    oldmoves = 

    for p in t:
        people_left = t[:]
        people_left.remove(p)
        oldmoves[p] = people_left

    if iterations == []:
        solution = min(map(lambda i: (abs(float(i)-halftotalscore), i), oldmoves.keys()))
        return (solution[1], sum(oldmoves[solution[1]]), oldmoves[solution[1]])

    for n in iterations:
        newmoves = 
        for total, roster in oldmoves.iteritems():
            for p in roster:
                people_left = roster[:]
                people_left.remove(p)
                newtotal = total+p
                if newtotal > halftotalscore: continue
                newmoves[newtotal] = people_left
        oldmoves = newmoves

    solution = min(map(lambda i: (abs(float(i)-halftotalscore), i), oldmoves.keys()))
    return (solution[1], sum(oldmoves[solution[1]]), oldmoves[solution[1]])

print team([90,200,100])
print team([2,3,10,5,8,9,7,3,5,2])
print team([1,1,1,1,1,1,1,1,1,9])
print team([87,100,28,67,68,41,67,1])
print team([1, 1, 50, 50, 50, 1000])

#output
#(200, 190, [90, 100])
#(27, 27, [3, 9, 7, 3, 5])
#(5, 13, [1, 1, 1, 1, 9])
#(229, 230, [28, 67, 68, 67])
#(150, 1002, [1, 1, 1000])

还请注意,我尝试使用 GS 的描述来解决此问题,但仅通过存储运行总数是不可能获得足够信息的。如果您同时存储项目数和总数,那么它将与此解决方案相同,只是您保留了不必要的数据。因为您只需要将 n-1 和 n 次迭代保持到 numplayers/2。

我有一个基于二项式系数的旧的详尽的(回顾历史)。它很好地解决了长度为 10 的示例问题,但后来我看到比赛的人长度达到了 100。

【讨论】:

@becomingGuru,我实现了一个可以正常工作的解决方案,看看它。 @tom10 实际上,您的解决方案对于 [87,100,28,67,68,41,67,1] 失败。它输出 223 236。不错的尝试。 @tom10,不,不是。当你的朋友犯错时,你会直接告诉他他错了吗?还是你告诉他怎么解决? 那么你的组合,这真的是在背包问题中尝试所有情况的实现吗? 来自问题:“每个测试用例都以空行开头,后跟N,即玩家总数。[...]总共最多有100名玩家(1一些比赛,例如即将到来的 IPSC ipsc.ksp.sk ,预先提供实际输入,但这不是 IOI、ACM-ICPC 等的工作方式.【参考方案12】:

问。给定一个整数的多集 S,有没有办法将 S 划分为 两个子集 S1 和 S2,使得 S1 中数字的 总和等于 S2 中数字的总和?

A.Set Partition Problem.

祝你好运。 :)

【讨论】:

【参考方案13】:

在较早的评论中,我假设设置的问题是易于处理的,因为他们在分配的时间内仔细选择了与各种算法兼容的测试数据。事实证明并非如此 - 相反,它是问题限制 - 不高于 450 的数字和不超过 50 个数字的最终集合是关键。这些与使用我在稍后的帖子中提出的动态编程解决方案解决问题兼容。其他算法(启发式算法,或组合模式生成器的详尽枚举)都无法工作,因为会有足够大或足够难的测试用例来破坏这些算法。老实说,这很烦人,因为其他解决方案更具挑战性,当然也更有趣。请注意,无需大量额外工作,动态规划解决方案只是说明对于任何给定总和是否有 N/2 的解决方案,但它不会告诉您任一分区的内容。

【讨论】:

【参考方案14】:

他们显然在寻找动态编程背包解决方案。因此,在我的第一次努力(我认为是一个非常好的原始启发式)和我的第二次努力(一个非常狡猾的精确组合解决方案之后,它适用于短数据集,甚至适用于多达 100 个元素的集合,只要 的数量)唯一值很低),我终于屈服于同侪的压力并写了他们想要的(不太难 - 处理重复的条目是最棘手的部分 - 我基于它的底层算法只有在所有输入都是唯一的情况下才有效- 我很高兴 long long 足够容纳 50 位!)。

因此,对于我在测试前两次尝试时汇总的所有测试数据和尴尬的边缘情况,它给出了相同的答案。至少对于我用组合求解器检查过的那些,我知道它们是正确的。但是我仍然没有提交错误的答案!

我不是要求任何人在这里修复我的代码,但如果有人能找到下面代码生成错误答案的案例,我将非常感激。

谢谢,

格雷厄姆

PS 此代码始终在时间限制内执行,但距离优化。我一直保持简单,直到它通过测试,然后我有一些想法可以加快速度,可能提高 10 倍或更多。

#include #define TRUE (0==0) #define FALSE (0!=0) 静态int调试=真; //int simple(const void *a, const void *b) // 返回 *(int *)a - *(int *)b; // int main(int argc, char **argv) 诠释 p[101]; 字符 *s,行 [128]; 长长的掩码,c0[45001],c1[45001]; int 技能,玩家,目标,i,j,测试,总计 = 0; 调试 = (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'd' && argv[1][2] == '\0'); s = fgets(行,127,标准输入); 测试 = atoi(s); 而(测试-> 0) 对于 (i = 0; i 0; i--) if (debug || (c0[i] & mask)) fprintf(stdout, "%d %d\n", i, total-i); 如果(调试) if (c0[i] & mask) printf("******** [");别的 printf("["); for (j = 0; j

【讨论】:

以上是关于将数字列表划分为2个相等总和列表的算法的主要内容,如果未能解决你的问题,请参考以下文章

将数字列表分成大致相等的总数

698. 划分为k个相等的子集(Python)

将列表拆分为 2 个相等和的列表

如何将一个巨大的列表分成相等的块并将条件应用于这些块?

Leetcode——划分为k个相等的子集(目标和)

随机红包算法