将一个数字列表分成两个等和列表的算法

25
有一个数字列表。 需要将该列表分成两个大小相等的列表,使它们的和之差最小。需要打印这两个列表的和。
#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/


2
这是一种装箱问题的变体吗?我记得这是一个NP完全问题。 - Jerry Penner
3
que = [1,50,50,100]应该得到100和101两个队伍。我觉得你的算法会得到51和150。 - Alex B
1
@S.Lott 这是一个编程竞赛中的练习问题。这是参考链接:http://www.codechef.com/problems/TEAMSEL/ 据我所知,我的理解是正确的。但系统标记为不正确。 - lprsd
2
@Alex B:当我运行它时,我得到了100和101。 - Glenn
@Alex B:根据您的输入,我正确地得到了100和101。 - lprsd
@becomingGuru,看看我的解决方案,我认为你会喜欢的。 - Unknown
14个回答

35

动态规划就是你要找的解决方案。

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

X轴: 数组中可达到的总和。最大值 = 所有数字之和 / 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 | | | | | | | | 2| 2| 3| | | | | // 2 3 | | | | | | | | | | | | | | |

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}

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

顺便说一句:这被称为背包问题

如果所有重量(w1、...、wn和W)都是非负整数,则可以使用动态规划在伪多项式时间内解决背包问题。


12
Pseudo-polynomial time(http://en.wikipedia.org/wiki/Pseudo-polynomial_time)意味着时间复杂度在输入数字的大小上是多项式级别的,但在输入长度上仍然不是多项式级别的。如果输入数字的大小是有限制的,则可以使用多项式时间算法。但如果没有限制,则无法使用多项式时间算法。例如,如果背包问题中的n个输入数字为2^0、2^1、...、2^(n-1),那么在动态规划解决方案的最后一步中,需要检查2^n个解决方案。 - dewtell
1
因为它基本上是正确的:有一个有效的动态规划算法可用。(您只需要为possible[nitems][sum]保留布尔值,而不仅仅是每个总和一个布尔值。) - ShreevatsaR
@Pranav:很简单:只需将第n+1个元素添加到所有可能的总和中。 - Georg Schölly
但这并没有回答提问者的问题,即所需总和应由集合中一半的元素组成。除非我们针对每个布尔值跟踪所有可能的元素集合,否则无法解决该问题。 - Pranav
@Pranav:我已经将我的答案更改为完整的解决方案。现在它使用了一个二维数组。 - Georg Schölly
显示剩余8条评论

6

新方案

这是一种带有启发式剪枝的广度优先搜索。树的深度限制为玩家数的一半。玩家总分数限制为总分数的一半。对于一个玩家池为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
@tom10,实际上你的解决方案对于[87,100,28,67,68,41,67,1]是失败的。它输出了223 236。不错的尝试。 - Unknown
@tom10,不是的。当你的朋友犯错时,你只是告诉他他错了吗?还是告诉他如何解决它? - Unknown
1
那么,根据你的组合,这是否真的是尝试在背包问题中的所有情况的一种实现? - tom10
1
问题中提到:“每个测试用例都以一个空行开头,接下来是N,即玩家的总数。[...] 总共最多有100名玩家(1 <= N <= 100)。 ”关于编程竞赛如何工作的一些背景信息:通常会提供一些(小)示例输入,但您提交的程序将根据问题陈述中提到的输入大小进行评估。(示例输入故意旨在简单。)某些比赛(例如即将举行的IPSC http://ipsc.ksp.sk/)提前提供实际输入,但IOI,ACM-ICPC等不是这样工作的。 - ShreevatsaR
显示剩余5条评论

4

问题:给定一个包含整数的多重集合S,是否有一种方式将S分成两个子集S1和S2,使得S1中数的总和等于S2中数的总和?

答案:集合划分问题

祝你好运,尽力去近似解决。:)


2

嗯,你可以找到一个多项式时间内解决百分比精度的解决方案,但要实际找到最优(绝对最小差异)解决方案,问题是NP完全问题。这意味着没有多项式时间的解决方案。因此,即使有相对较小的数字列表,计算成本也太高了无法解决。如果您真的需要解决方案,请查看一些近似算法。

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


这是错误的。这是可以使用动态规划来解决的背包问题。 - Georg Schölly
我认为这不是一个子集和问题……虽然我会自由承认,我已经离开这个领域太久了,不能肯定地说。我喜欢GS提出的动态规划方法。你能解释一下为什么那种方法行不通吗? - Beska
2
@gs:这不是错的。你可以把它看作是子集和问题或背包问题(技术上,它被称为数分割问题),因为所有NP完全问题都是等价的。 :-)而且,这个问题说明了为什么重要的是不要被“NP完全”这个术语冲昏头脑:尽管在最坏情况下没有已知的多项式时间复杂度的算法(输入位数),但动态规划算法表明,可以在输入数码本身的多项式时间内完成。这与背包问题相同:查找“伪多项式时间”。 - ShreevatsaR

1
请注意,这也是一种启发式方法,并且我将排序从函数中移出。
 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)

1
一个您的方法无法正常工作的测试用例是:
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

你提供了一个测试用例来证明这个错误。已点赞。 我之所以成对使用,是因为两个列表中需要有相等数量的条目。 - lprsd
是的,但我认为任何简单的解决方案都将是一种启发式方法,在这里最好的结果应该是1002 150。 - odwl
@odwl:我同意你的观点。当你成对地执行此操作时,你会得到101、1051,而逐项执行则会得到150、1002。 - tom10
@becomingGuru,我实现了一个能够正确工作的解决方案,请看一下。 - Unknown
@tom10 如果你看了我的帖子,你就会知道。 - Unknown
显示剩余3条评论

1

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

因此,对于我测试前两个尝试时收集的所有测试数据和棘手的边缘情况,它给出了相同的答案。至少对于我使用组合求解器检查过的那些,我知道它们是正确的。但是我仍然无法通过提交,得到了错误的答案!

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

谢谢,

Graham

PS 这段代码总是在时间限制内执行,但它远非最优化。我会保持简单,直到它通过测试,然后我有一些想法来加速它,或许可以提高10倍甚至更多。

#include <stdio.h>
#define TRUE (0==0) #define FALSE (0!=0)
static int debug = TRUE;
//int simple(const void *a, const void *b) { // return *(int *)a - *(int *)b; //}
int main(int argc, char **argv) { int p[101]; char *s, line[128]; long long mask, c0[45001], c1[45001]; int skill, players, target, i, j, tests, total = 0;
debug = (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'd' && argv[1][2] == '\0');
s = fgets(line, 127, stdin); tests = atoi(s); while (tests --> 0) {
for (i = 0; i < 45001; i++) {c0[i] = 0LL;}
s = fgets(line, 127, stdin); /* blank line */ s = fgets(line, 127, stdin); /* no of players */ players = atoi(s); for (i = 0; i < players; i++) {s = fgets(line, 127, stdin); p[i] = atoi(s);}
if (players == 1) { printf("0 %d\n", p[0]); } else {
if (players&1) p[players++] = 0; // odd player fixed by adding a single player of 0 strength //qsort(p, players, sizeof(int), simple);
total = 0; for ( i = 0; i < players; i++) total += p[i]; target = total/2; // ok if total was odd and result rounded down - teams of n, n+1 mask = 1LL << (((long long)players/2LL)-1LL);
for (i = 0; i < players; i++) { for (j = 0; j <= target; j++) {c1[j] = 0LL;} // memset would be faster skill = p[i]; //add this player to every other player and every partial subset for (j = 0; j <= target-skill; j++) { if (c0[j]) c1[j+skill] = c0[j]<<1; // highest = highest j+skill for later optimising } c0[skill] |= 1; // so we don't add a skill number to itself unless it occurs more than once for (j = 0; j <= target; j++) {c0[j] |= c1[j];} if (c0[target]&mask) break; // early return for perfect fit! }
for (i = target; i > 0; i--) { if (debug || (c0[i] & mask)) { fprintf(stdout, "%d %d\n", i, total-i); if (debug) { if (c0[i] & mask) printf("******** ["); else printf(" ["); for (j = 0; j <= players; j++) if (c0[i] & (1LL<<(long long)j)) printf(" %d", j+1); printf(" ]\n"); } else break; } } } if (tests) printf("\n"); } return 0; }

1

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

它是NP完全问题,可以使用伪多项式dp算法解决。伪多项式中的“伪”指的是运行时间取决于权重范围。

通常情况下,您需要先确定是否存在精确解,然后才能采用启发式解决方案。


0
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

这对我来说看起来太复杂了。 - odwl

0
你可以通过使用以下方法来加强循环:
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))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接