【问题标题】:Algorithm: Optimal combination of values to stay within range算法:值的最佳组合以保持在范围内
【发布时间】:2013-08-08 17:22:55
【问题描述】:

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

  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 个值,我需要十分之一秒的结果。

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

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

【问题讨论】:

  • 每次淘汰都需要保持在范围内,还是只需要最终的数字列表?例如,如果范围是 (-1, 1) 而你有 (5, -5),你能同时消除还是不消除?
  • @rogaos 由于示例的第二步给出的总和为 21,我认为最终数字需要在范围内。
  • @rogaos:只有最终的号码列表需要在范围内。
  • 我认为更清楚地说,“2. 这些值的总和在 给定 范围内 (x, y)”,以明确 (x, y) 是输入的一部分,而不是你可以从输入中计算出来的东西。

标签: algorithm math range combinations subset-sum


【解决方案1】:

我很想向后执行此操作,但我不确定是否允许(请参阅我的评论。)

所以不是一个一个地消除值,让我们找到总和在范围内的最小子列表!

有一个问题——subset sum problem 是 np-complete,所以这种方法也是。 (想象一下你的范围为 0 的情况,也是同样的问题。)

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

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

如果允许您进行违反规则的淘汰,只要最终列表在范围内,请查看此question

编辑:这里有一些 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))

【讨论】:

  • 但是如果问题是 O(2^(N/2)) 并且我有 200 个值需要 2 的 100 次迭代,对吗?在普通台式计算机上,这不会在十分之一秒内完成,是吗?
  • 把问题转化为“不是一个一个地消除值,而是找到总和在范围内的最小子列表!”很有见地。
  • @RoelVlemmings 绝对不是,但我认为如果你想出一个更快的解决方案,你应该发布。检查下面的动态编程是否更快!
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

    最坏的情况,你需要检查所有的组合,即 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;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-05-20
      • 2021-09-27
      • 1970-01-01
      • 2019-01-07
      • 1970-01-01
      • 2013-03-12
      • 1970-01-01
      相关资源
      最近更新 更多