【问题标题】:find optimal sum of elements in list of numbers that are > a given number在大于给定数字的数字列表中找到元素的最佳总和
【发布时间】:2016-09-08 23:24:14
【问题描述】:

我有一个数字列表,降序排列,数组大小可变,可以出现任何数字(通常小于 1000)

给定一个输入数字 (x),我需要找到列表中值的最佳组合,即比 x 大尽可能小的数量。我一直在阅读有关 NP 优化和求和子集类型问题的信息,但还没有找到解决方案。我找到了一些近似算法的伪代码,但我想从给定的数字列表中找到确切的最佳解决方案。 谢谢

【问题讨论】:

  • 数字是整数和/或有符号吗?
  • @trincot - 只是整数

标签: algorithm optimization dynamic-programming


【解决方案1】:

这听起来很像knapsack problem。如果是这样,那么找到一个精确的解决方案并不容易。

这是为什么呢?这是因为最好的解决方案可能由您的数字的任何子集组成。有 2^N 个子集,因此搜索解的空间非常大。

它有多大?

Items | Search Space          | How this feels
16    | 65,536                | I can do it!
32    | 4,294,967,296         | Better go get lunch
40    | 1,099,511,627,776     | If I only had a supercomputer
50    | 1,125,899,906,842,624 | I will watch as the last stars day

有一些技巧和方法可以减少搜索空间,但你仍然会遇到项目数量少的问题。

鉴于此,您应该:

  1. 只选择小问题并使用穷举搜索,因为它易于实施。
  2. 了解分支定界和其他搜索空间缩减技术
  3. 大约。每个人都这样做。

【讨论】:

  • 我认为这是一个背包问题..关于如何实现详尽搜索或近似的好方法有什么建议吗? “足够接近”就足够我想做的事情了。谢谢!
  • 我不同意这是复杂性的问题2^N
  • 是的,我认为你是对的。抱歉,我想我想太多了
  • @trincot:很好。而且毫无意义。你为什么不提供一个论据为什么它不是?我看到您发布了一些黑盒代码;它的时间复杂度是多少?
  • @Richard,请参阅我的更新答案以获得一些答案,尽管我没有证据证明我的算法的时间复杂度是多少。但是您的回答似乎表明算法在输入 50 个元素时会遇到问题,但我建议的算法在具有 1000 个元素的数组上运行良好,并且验证和数的统计数据不遵循 2^N 模式全部。我没有注意到任何“项目数量少的大问题”
【解决方案2】:

从您的 cmets 中,我了解到输入数组具有无符号(整数)。

这个问题似乎和subset sum problem with non-negative integers 没有太大区别,可以在多项式时间内解决。

我发现这是一个性能相当不错的算法,可以找到最佳解决方案:

伪代码算法

For each element of the array:
    select this element  
    if sum of selected <= x:
        # Sum is too small, so add more (smaller) term(s)
        execute algorithm recursively for the remaining part of array
    else if < sum in best solution so far:
        # Sum is closer to target, so this is currently the best
        best solution = current selected terms
    # Back-track: remove this term from the sum
    unselect this element
return best solutiuon

当算法被递归调用时,之前选择的项仍然被选中,并且在循环中又选择了一个项。它可以再次递归等等。选择的术语的总数对应于递归的深度。

递归切割许多组合有两种方式:

  • when a high value term is selected, the remaining value for crossing the target value becomes relatively small, reducing the possibilities of which other terms can contribute to a (better) solution;
  • when a lower value term is selected, the remaining number of terms is relatively small (because of the order), and so also the possibilities are reduced.

这表明时间复杂度小于 O(2n),这将是必须研究所有可能的组合时的复杂度(或常数的一部分)。

实施

这里有一个 JavaScript 实现,所以你可以运行它。它提供了一个随机化按钮,因此您可以使用随机数和随机目标值生成任意给定长度的数组。

算法似乎O(n.logn)时间的顺序运行,只需查看它检查的平均组合数。这当然不是证据。

代码中的cmets应该给出说明。

// Main  algorithm
function solve(a /* array of int */, x /* int */) {
    // Initialise
    var best = {sum: a[0] * a.length, numSumsVerified: 0, numTerms: 0, terms: []};
    var current = {sum: 0, terms: []};

    function recurse(start) {
        var ok = start < a.length;
        for (var i = start; i < a.length && best.sum > x + 1 && ok; i++) {
            // Use this term for the sum
            current.sum += current.terms[current.terms.length] = a[i];
            // Keep statistics of how many combinations we check
            best.numSumsVerified++;
            if (current.sum <= x) {
                // Sum is too small, so add more (smaller) term(s)
                ok = recurse(i+1);
            } else if (current.sum < best.sum) {
                // Sum is closer to target, so this is currently the best
                best.sum = current.sum;
                best.terms = current.terms.slice(0);
            }
            // Back-track: remove this term from the sum
            current.sum -= current.terms.pop();
        }
        return ok || i > start + 1;
    }

    // start the search, and capture errors
    try {
        recurse(0);
    } catch (ex) {
        best.error = 'Too much recursion!';
        best.sum = null;
        return best;
    }
    best.numTerms = best.terms.length;
    // if no solution, set error message
    if (!best.terms.length) {
        best.error = 'no solution';
        best.sum = null;
    }
    return best;
}

// Utility for randomizing
function createRandomNumbers(limit, count) {
    res = [];
    while (count--) res.push(Math.floor(Math.random() * limit));
    return res;
}

// I/O

var inputA = document.querySelector('#a');
var inputX = document.querySelector('#x');
var buttonSolve = document.querySelector('#solve');
var inputSize = document.querySelector('#size');
var buttonRandom = document.querySelector('#randomize');
var output = document.querySelector('pre');

buttonSolve.onclick = function() {
    // Get input
    var a = inputA.value.match(/\d+/g).map(Number);
    var x = Number(inputX.value);
    // Sort descending
    a.sort(function(a,b) { return b-a; });
    // Solve
    var result = solve(a, x);
    // Output
    inputA.value = a.join(' '); // just for reformatting
        // Reduce detail when many items
    if (result.terms.length > 100) result.terms = '(too many to display)';
    output.textContent = JSON.stringify(result, null, 4);
};

buttonRandom.onclick = function() {
    // Generate random input
    var size = Number(inputSize.value);
    var limit = size * 20;
    var a = createRandomNumbers(limit, size).sort((a,b) => b-a);
    var sum = a.reduce((a,b) => a+b);
    var x = createRandomNumbers(sum, 1).pop();
    // Populate the input boxes
    inputA.value = a.join(' ');
    inputX.value = x;
    // Trigger click on "solve" button
    setTimeout(buttonSolve.click.bind(buttonSolve), 0);
}
Enter list of integers: <input id="a" size="50" value="18 13 12 10 9 8 6 6 1 0"><br>
Sum must be larger than: <input id="x" size="10" value="16"><br>
<button id="solve">Solve</button><br>
Desired array size: <input id="size" size="6" value="50">
<button id="randomize">Random Input</button>
<pre></pre>

由于此算法对添加到组合中的每个术语执行递归调用,因此对于大型输入数组,递归可能会深入。在某个时刻,它可能会达到堆栈限制。在我的浏览器中,这个限制经常接近 10,000 个元素的数组大小。如果需要将这种算法用于如此大的数组,则可能可以在不使用递归的情况下重写该算法。

【讨论】:

  • 谢谢,这看起来不错。我正在使用 Java,但我认为做这样的事情应该不会太难。我希望....
  • 我做了一些更新。我必须决定没有解决方案的条件是错误的。现已更正。同时您现在可以指定随机数组的大小。您会看到,即使对于 1000 个数字的数组,它也会在可接受的时间内运行。
猜你喜欢
  • 2012-11-20
  • 2021-05-12
  • 1970-01-01
  • 2012-11-10
  • 2014-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-09
相关资源
最近更新 更多