从列表中移除数字而不改变总和

7 投票
5 回答
1417 浏览
提问于 2025-04-15 17:11

我有一串数字(比如说:[-1, 1, -4, 5]),我需要从这个列表中去掉一些数字,但总和不能变。我想去掉绝对值最大的数字,确保总和不变。在这个例子中,去掉 [-1, -4, 5] 后,剩下的就是 [1],这样总和就没变。

我写了一个简单的方法,就是找出所有可能的组合,看看哪些组合能去掉数字而不改变总和,最后找出去掉的绝对值最大的组合。但是这样做会很慢,因为实际的列表会比这个大得多。

这是我用来找组合的代码:

from itertools import chain, combinations

def remove(items):
    all_comb = chain.from_iterable(combinations(items, n+1) 
                                   for n in xrange(len(items)))
    biggest = None
    biggest_sum = 0
    for comb in all_comb:
        if sum(comb) != 0:
            continue # this comb would change total, skip
        abs_sum = sum(abs(item) for item in comb)
        if abs_sum > biggest_sum:
            biggest = comb
            biggest_sum = abs_sum
    return biggest

print remove([-1, 1, -4, 5])

它正确地输出了 (-1, -4, 5)。不过我在寻找一种更聪明、更高效的解决方案,而不是遍历所有可能的组合。

有没有什么好主意呢?

5 个回答

0

你的需求没有说明这个函数是否可以改变列表的顺序。这里有一个可能的做法:

def remove(items):
    items.sort()
    running = original = sum(items)
    try:
        items.index(original) # we just want the exception
        return [original]
    except ValueError:
        pass
    if abs(items[0]) > items[-1]:
        running -= items.pop(0)
    else:
        running -= items.pop()
    while running != original:
        try:
            running -= items.pop(items.index(original - running))
        except ValueError:
            if running > original:
                running -= items.pop()
            elif running < original:
                running -= items.pop(0)
    return items

这个方法会先把列表排序(大的项目会排在最后,小的项目会排在最前),然后计算总和,并从列表中移除一个项目。接着,它会继续移除项目,直到新的总和等于原来的总和。如果你想保持顺序,可以写一个包装函数:

from copy import copy

def remove_preserve_order(items):
    a = remove(copy(items))
    return [x for x in items if x in a]

不过,如果你真的想保持顺序,最好用 collections.deque 来重写这个代码。如果你能保证列表中的元素是唯一的,使用 set 会让你获得很大的性能提升。

我们可能还可以写一个更好的版本,每次遍历列表找到与当前总和最接近的两个数字,然后移除其中更接近的那个,但这样可能会导致性能变成 O(N^2)。我认为这个代码的性能是 O(N*log(N)),因为它只需要对列表进行排序(希望 Python 的列表排序不是 O(N^2)),然后再计算总和。

4
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2009 Clóvis Fabrício Costa
# Licensed under GPL version 3.0 or higher

def posneg_calcsums(subset):
    sums = {}
    for group in chain.from_iterable(combinations(subset, n+1) 
                                     for n in xrange(len(subset))):
        sums[sum(group)] = group
    return sums

def posneg(items):
    positive = posneg_calcsums([item for item in items if item > 0])
    negative = posneg_calcsums([item for item in items if item < 0])
    for n in sorted(positive, reverse=True):
        if -n in negative:
            return positive[n] + negative[-n]
    else:
        return None

print posneg([-1, 1, -4, 5])
print posneg([6, 44, 1, -7, -6, 19])

这个方法很好用,而且比我最开始的方法快多了。感谢Alon提供的维基百科链接,还有在#python的irc频道上给我提示的ivazquez|laptop,让我找到了这个解决方案。

我觉得这个方法还可以进一步优化——我想找到一种方法,一旦找到了解决方案,就停止计算那些耗时的部分。我会继续尝试的。

11

如果你把这个问题重新定义为寻找一个子集,使得这个子集的总和等于整个集合的总和,你会发现这其实是一个 NP-困难的问题,(子集和问题)。

所以,这个问题没有简单的解决办法,也就是说,没有一种能在合理时间内解决这个问题的方法。

撰写回答