4

我有以下数学问题,我在应用程序中需要它,我想知道是否有一种有效的方法来找到最佳解决方案而不是近似值。

  1. 我有一个正负值列表。
  2. 这些值的总和在 (x, y) 范围内。
  3. 我想知道我可以消除的最大值数,以便剩余值的总和保持在范围内。

例子:

Values: -10, -5, -2, 7, 9, 15
Sum: 14
Range: (10, 18)

Eliminate -2 => SUM = 16
Eliminate -5 => SUM = 21
Eliminate 7 => SUM = 14
Eliminate -10 => SUM = 24
Eliminate 9 => SUM = 15

消除 15 将使 SUM = 0,超出范围。消除了 5 个值。

而如果我从消除 15 开始,然后是 -10、-5、-2,我只能消除 4 个值。

我曾经写过一个算法,它简单地尝试了所有可能的组合,但是一旦你有 25 个或更多的值,它的性能就会迅速下降。对于 100-200 个值,我需要十分之一秒的结果。

目前,我在绝对基础上将值从小到大排序,然后逐个消除它们,直到总和不再在范围内。显然,这可能并不总是给出最佳解决方案。

如果这不是此类问题的正确位置,并且您可以参考另一个论坛,那也会有所帮助。

4

3 回答 3

3

我很想向后做,我不确定是否允许(请参阅我的评论。)

因此,与其逐个消除值,不如找到总和在该范围内的最小子列表!

有一个问题——子集和问题是 np 完全的,所以这种方法也是。(想象一下你的范围是 0 的情况,这是同样的问题。)

有一个已知的算法可以在 O(2 N/2 )中解决这个问题。我将模拟一些 Python 代码,但与此同时,维基百科页面应该会有所帮助。由于您想在一个范围内找到最少的列表,因此显然需要进行一些修改。

本质上,您将列表分成两个长度为 N/2 的任意子列表(其中您的列表有 N 个元素。)然后在每个列表中生成所有子集,并计算它们的总和。(在这里,我会将子集及其总和存储在字典中,这样您就知道还剩下哪些数字。由于您只想找到最小的数字,因此我还将消除所有与较小的子集具有相同总和的子集。)对这些列表进行排序,然后向前和向后遍历,直到找到所有适合该范围的总和。最后,找出哪个包含最少的元素,你就可以开始了!

如果只要最终列表在范围内,您就可以进行违反规则的消除,请查看此问题

编辑:这里有一些 Python。这是:

  • 未经测试

  • Python,所以不是特别快

  • 显然不是最优的

  • 急需重构

但我认为作为一个总体思路,您将能够获得最快的算法。我很想看到一个更快的概念!

>>> from itertools import combinations, chain
>>> 
>>> available = [-10, -5, -2, 7, 9, 15]
>>> target = (10, 18)
>>> 
>>> 
>>> 
>>> def powerset(iterable): # from https://stackoverflow.com/questions/374626/how-can-i-find-all-the-subsets-of-a-set-with-exactly-n-elements
...     xs = list(iterable)
...     # note we return an iterator rather than a list
...     return chain.from_iterable(combinations(xs, n) for n in range(len(xs)+1))
... 
>>> 
>>> def getMinList(available, target):
...     middleIndex = len(available)/2
...     l1 = available[:middleIndex]
...     l2 = available[middleIndex:]
...     dict1 = {}
...     dict2 = {}
...     for subset in powerset(l1): # reverse so only the smallest subsets are used.
...         total = sum(subset)
...         if total not in dict1:
...             dict1[total] = subset
...     for subset in powerset(l2):
...         total = sum(subset)
...         if total not in dict2:
...             dict2[total] = subset
...     sortedDict1 = sorted(dict1.iteritems())
...     sortedDict2 = sorted(dict2.iteritems())
...     resultList = ()
...     minValues = middleIndex * 2
...     for k1, v1 in sortedDict1:
...         for k2, v2 in reversed(sortedDict2):
...             sumOfSubsets = k1 + k2
...             if sumOfSubsets <= target[1] and sumOfSubsets >= target[0]:
...                 newTuple = v1 + v2
...                 lenNewTuple = len(newTuple)
...                 if (lenNewTuple) < minValues:
...                     resultList = ((sumOfSubsets, newTuple))
...                     minValues = lenNewTuple
...     return resultList
... 
>>> getMinList(available, target)
(15, (15,))
>>> 
>>> target = (10, 10)
>>> 
>>> getMinList(available, target)
(10, (-5, 15))
>>> 
>>> target = (19, 22)
>>> 
>>> getMinList(available, target)
(22, (7, 15))
于 2013-08-08T17:52:00.103 回答
1

更糟糕的情况是,您需要检查所有组合,即 O(2^n)。但是,如果您开始检查最小的子列表,您可以在找到一个后停止。这是我的 C++ 编写。它可以改善内存使用,但需要更多工作。所以取决于你的输入,它可能非常快或很慢。

using namespace std;

int compare(int x, int r1, int r2)
{`
    if (x < r1) return -1;
    if (x > r2) return 1;
    return 0;
}

// assume sorted v, binary search
bool hasMemInRange(const vector<int>& v, int r1, int r2)
{
    int b, e, c, r;

    b=0; e=v.size();
    while(e > b) {
        c = (b+e)/2;
        r = compare(v[c], r1, r2);
        if (r < 0) {
            b += max(1, (e-b)/2);
        } else if (r > 0) {
            e -= max(1, (e-b)/2);
        } else {
            return true;
        }
    }
    return false;
}
struct InputNode {
    vector<int> l;
    int         r1, r2;
};

// assume v is sorted
int maxRemoval(const vector<int>& v, int r1, int r2)
{
    if (compare(0, r1, r2) == 0) return v.size();

    if (hasMemInRange(v, r1, r2)) return (v.size() - 1);

    queue<InputNode> q;
    InputNode node;

    node.l = v;
    node.r1 = r1;
    node.r2 = r2;
    q.push(node);

    while(! q.empty()) {
        InputNode& n = q.front();

        if (n.l.size() == 1) {
            return 0;
        }

        for (int i=0; i<n.l.size(); ++i) {
            vector<int> nv = n.l;
            nv.erase(nv.begin() + i);
            node.l = nv;
            node.r1 = r1-n.l[i];
            if (hasMemInRange(nv, node.r1, node.r2)) {
                return (nv.size() - 1);
            }
            q.push(node);
        }
        q.pop();
    }
}

int list_ints[] = {-10, -5, -2, 7, 9, 15 };

int main()
{
    vector<int> l(list_ints, list_ints + sizeof(list_ints)/sizeof(int));

    for (auto i: list_ints) cout << i << "    ";
    cout << endl << endl;

    cout << maxRemoval(l, 10, 18) << endl;
    return 0;
}
于 2013-08-09T15:41:23.890 回答
1

使用动态编程(通过记忆实现),您可以使用以下内容:

class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        return self.memo[args]        

def maxsubset(values, min_sum, max_sum):
    target_range = range(min_sum, max_sum+1)

    @Memoize
    def maxsubsetsize(target_sum, current_value_index=len(values)-1):
        if current_value_index < 0:
            if target_sum == 0:
                return 0
            else:
                return float("-inf")

        withit = maxsubsetsize(target_sum - values[current_value_index], current_value_index-1) + 1
        without = maxsubsetsize(target_sum, current_value_index-1)
        return max(withit, without)

    result_sum = max(target_range, key=maxsubsetsize)
    setsize = maxsubsetsize(result_sum)

    result = []
    for i in reversed([x-1 for x in xrange(len(values))]):
        s = maxsubsetsize(result_sum, i)
        if s < setsize:
            result.append(values[i+1])
            setsize -= 1
            result_sum -= values[i+1]

    return result

用法:

>>> values = [-10, -5, -2, 7, 9, 15]
>>> min_sum = 10
>>> max_sum = 18

>>> xs = maxsubset(values, min_sum-sum(values), max_sum-sum(values))
>>> print xs
[9, 7, -2, -5, -10]
>>> print "sum:", sum(xs)
-1

如果可以达到特定的总和,您可以添加额外的检查。所有可用的负值给出总和的下限,所有可用的正数给出一个上限。

于 2013-08-09T10:38:26.803 回答