从列表中删除数字而不更改总和

Posted

技术标签:

【中文标题】从列表中删除数字而不更改总和【英文标题】:remove numbers from a list without changing total sum 【发布时间】:2010-12-28 07:22:39 【问题描述】:

我有一个数字列表(例如:[-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)。但是,我正在寻找一些比遍历所有可能的项目组合更聪明、更有效的解决方案。

有什么想法吗?

【问题讨论】:

在这种情况下,如果我们观察到总和是这个列表中的一个项目,那就是胜利了。如果我们有sum(items)abs_sum(items),那么尝试使用列表中的 1、2、3 等元素加起来可能会更有效,即从空列表案例而不是完整列表开始( ?) 您应该保存smallest_abs_sum 而不是biggest_sum。考虑:[1,-1,100,-100]. @J.F. Sebastian:如果输入是[1,-1,100,-100],它应该删除所有内容(202abs_sum)保持总和0 @nosklo:我明白了:您的remove() 函数返回要删除的项目,而不是最终结果列表。 【参考方案1】:

如果你把问题重新定义为找到一个总和等于完整集值的子集,你就会意识到这是一个 NP-Hard 问题,(subset sum)

所以这个问题没有多项式复杂性解决方案。

【讨论】:

感谢您的回答和良好的链接。***似乎暗示有一个伪多项式时间动态规划解决方案,这意味着我会存储部分解决方案以帮助未来计算,但通过阅读它我无法理解(它是英文形式,英语不是我的自然语言)。你能帮我理解它,这样我就可以使用这个方法编写一个算法并针对我的方法进行测试吗?好像会更快。 我想我明白了!看我的回答。【参考方案2】:
#!/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 提供了一个很好的提示,让我找到了解决方案。

我认为它可以进一步优化 - 一旦找到解决方案,我想要一种方法来停止计算昂贵的部分。我会继续努力的。

【讨论】:

非常好的实现!腺体你已经解决了;-) @Alon:我想我可以得到进一步的优化——有什么想法吗? 您的解决方案假定sum(items) == 0 是否正确? @J.F.塞巴斯蒂安:没有。如果sum(items) == 0 这意味着我可以删除所有内容......所以它假设sum(items) != 0 @nosklo:我是这么认为的。为什么然后sum(positive[n]+negative[-n]) == 0(其中sum(positive[n]) == n and sum(negative[-n]) == -n定义)。【参考方案3】:

您的要求没有说明该功能是否允许更改列表顺序。这是一种可能性:

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))然后得到总和。

【讨论】:

有趣的代码。顺序对我来说并不重要。但是我有重复的项目计入总和,所以我认为我不能使用集合。您的代码适用于我的原始数字(返回 [1])并且速度非常快。但是当我用[6, 44, 1, -7, -6, 19] 尝试它时(我希望它删除(6, 1, -7) 返回[-6, 19, 44],保持相同的总和57)它在最后一个IndexError: pop from empty list 上失败running -= items.pop(0)。你知道有什么办法可以解决这个问题吗?感谢您的帮助。 之所以这样做,是因为我的版本只尝试一个订单和一个订单。您可以制作一个递归版本,但您必须将函数拆分为两个函数(执行设置工作的部分,以及循环和递归的部分)。如果你愿意,我可以很快做出一些东西,但你可能会失去一些效率。但是,让我们在开始之前编写代码而不是猜测效率,好吗?【参考方案4】:

我不使用 Python 编程,所以我很抱歉没有提供代码。但我想我可以帮助算法:

    求和 将具有最低值的数字相加,直到得到相同的总和 其他所有内容都可以删除

希望对你有帮助

【讨论】:

谢谢。你能给我一个例子来说明如何做到这一点吗?我的意思是,如果我用[6, 44, 1, -7, -6, 19] 运行它,我希望它会删除(6, 1, -7) 留下[-6, 19, 44],会发生这种情况吗?【参考方案5】:

这可以使用整数规划来解决。您可以为每个列表元素 x_i 定义一个二进制变量 s_i 并最小化 \sum_i s_i,受限于 \sum_i (x_i*s_i) 等于列表的原始总和的约束。

这是在 R 中使用 lpSolve 包的实现:

library(lpSolve)
get.subset <- function(lst) 
  res <- lp("min", rep(1, length(lst)), matrix(lst, nrow=1), "=", sum(lst),
            binary.vec=seq_along(lst))
  lst[res$solution > 0.999]

现在,我们可以用几个例子来测试它:

get.subset(c(1, -1, -4, 5))
# [1] 1
get.subset(c(6, 44, 1, -7, -6, 19))
# [1] 44 -6 19
get.subset(c(1, 2, 3, 4))
# [1] 1 2 3 4

【讨论】:

以上是关于从列表中删除数字而不更改总和的主要内容,如果未能解决你的问题,请参考以下文章

Tesseract 输出从非常清晰的图像中更改、添加和删除数字

克隆 div 并删除类而不更改原始类

我可以从 ComboBox 中删除项目而不更改其他项目的索引吗?

如何按特定顺序从列表中删除字符串而不输入其值? [复制]

检查数字不是列表中 2 个整数的总和

如何从 ListView 中删除项目而不从数据库中删除它?