【问题标题】:Finding all possible combinations whose sum is within certain range of target查找总和在目标特定范围内的所有可能组合
【发布时间】:2019-06-29 17:17:57
【问题描述】:

所以我与一些同事交谈,我目前遇到的问题实际上非常具有挑战性。这个问题背后的背景与质谱和软件给出的不同峰的结构分配有关。

但是要分解成优化问题,我有一定的目标值。我还有一个各种输入的列表,我希望它们的总和尽可能接近目标。

举个例子,这就是我所拥有的。

List of inputs: [18.01, 42.01, 132.04, 162.05, 203.08, 176.03]

Target value: 1800.71

我想找出所有可能的输入组合,其总和在 1800.71 的 0.5 以内。所以总和可以在 1800.21 和 1801.21 之间的任何地方。

我已经知道两个输入可能是:

[18.01, 162.05, 162.05, 162.05, 162.05, 162.05, 162.05, 162.05, 162.05, 162.05, 162.05, 162.05] **which gives a sum of 1800.59**

[18.01, 18.01, 203.08, 203.08, 203.08, 162.05, 203.08, 18.01, 18.01, 18.01, 18.01, 18.01, 18.01, 18.01, 18.01, 18.01, 18.01, 42.01, 162.05, 203.08, 203.08] **which gives a sum 1800.71**

我不想找到让我尽可能接近目标值的组合;我对目标值 0.5 以内的所有可能组合感兴趣。

如果有人能帮我解决这个问题,我将不胜感激!

【问题讨论】:

  • 听起来您正在寻找coin change problem的某种变体
  • 查看itertools 的文档,尤其是combinationspermutationsproductchain
  • 数据集有多大?对于长度为 1 和 n 的快速而肮脏的排列将起作用,否则实现动态规划,如硬币找零问题所示。
  • @MatsLindh 数据集不大。我只有几个目标值,我想找到输入的组合。挑战在于我试图找到总和接近目标值的输入组合。
  • @Alan 问题是,您可以选择多少个数字。我觉得这个问题的复杂度等级相当高,它不会轻易超过 10-20 个数字。我认为 python 是解决这类问题的错误语言,因为它的速度非常慢。

标签: python python-3.x


【解决方案1】:

与其允许多个值,不如为每个值计算一个整数因子会快得多。

对于你的问题,我得到了 988 个结果。

import math
import time

def combinator(tolerance, target, inputs):

    # Special case for inputs with one element, speeds up computation a lot
    if len(inputs) == 1:
        number = inputs[0]
        result_min = int(math.ceil((target-tolerance)/number))
        result_max = int(math.floor((target+tolerance)/number))
        for factor in range(result_min, result_max+1):
            yield [factor]
        return

    # Special case for no inputs, just to prevent infinite recursion 
    if not inputs:
        return

    number = inputs[-1]
    max_value = int(math.floor((target + tolerance)/number))

    for i in range(max_value+1):
        for sub_factors in combinator(tolerance, target-i*number, inputs[:-1]):
            sub_factors.append(i)
            yield sub_factors

def main():
    inputs = [18.01, 42.01, 132.04, 162.05, 203.08, 176.03]
    target = 1800.71

    tolerance = 0.5

    t_start = time.perf_counter()
    results = list(combinator(tolerance, target, inputs))
    t_end = time.perf_counter()

    for result in results:
        result_str = ""
        result_value = 0
        for factor, value in zip(result, inputs):
            if not factor:
                continue
            if result_str != "":
                result_str += " + "
            result_str += "{}* {}".format(factor, value)
            result_value += factor*value
        print("{:.2f}".format(result_value) + " =\t[" + result_str + "]") 

    print("{} results found!".format(len(results)))
    print("Took {:.2f} milliseconds.".format((t_end-t_start)*1000))

if __name__ == "__main__":
    main()
1801.00 =   [100* 18.01]
1800.96 =   [93* 18.01 + 3* 42.01]
1800.92 =   [86* 18.01 + 6* 42.01]
...
1800.35 =   [5* 18.01 + 3* 42.01 + 9* 176.03]
1800.33 =   [2* 42.01 + 1* 132.04 + 9* 176.03]
1800.35 =   [3* 18.01 + 1* 162.05 + 9* 176.03]
988 results found!
Took 11.48 milliseconds.

我还在 Rust 中重新实现了相同的算法。

您的问题的表现:

  • Python:~12 毫秒
  • 生锈:~0.7 毫秒

代码如下:

use std::time::Instant;

fn combinator(tolerance : f32, target: f32, inputs: &[f32]) -> Vec<Vec<i32>>{

    let number = match inputs.last() {
        Some(i) => i,
        None => return vec![]
    };

    if inputs.len() == 1 {
        let result_min = ((target-tolerance)/number).ceil() as i32;
        let result_max = ((target+tolerance)/number).floor() as i32;
        return (result_min..=result_max).map(|x| vec![x]).collect();
    }

    let max_value = ((target + tolerance)/number).floor() as i32;

    let mut results = vec![];
    for i in 0..=max_value {
        for mut sub_factors in combinator(tolerance, target - i as f32 * number, &inputs[..inputs.len()-1]) {
            sub_factors.push(i);
            results.push(sub_factors);
        }
    }

    results
}

fn print_result(factors: &[i32], values: &[f32]){
    let sum : f32 = factors.iter()
        .zip(values.iter())
        .map(|(factor,value)| *factor as f32 * *value)
        .sum();
    println!("{:.2} =\t[{}]", sum,
             factors.iter()
                    .zip(values.iter())
                    .filter(|(factor, _value)| **factor > 0)
                    .map(|(factor, value)| format!("{}* {}", factor, value))
                    .collect::<Vec<String>>()
                    .join(", "));
}

fn main() {
    let inputs = vec![18.01, 42.01, 132.04, 162.05, 203.08, 176.03];
    let target = 1800.71;

    let tolerance = 0.5;

    let t_start = Instant::now();
    let results = combinator(tolerance, target, &inputs);
    let duration = t_start.elapsed().as_micros() as f64;

    for result in &results {
        print_result(&result, &inputs);
    }

    println!("{} results found!", results.len());
    println!("Took {} milliseconds", duration / 1000.0);
}
1801.00 =   [100* 18.01]
1800.96 =   [93* 18.01, 3* 42.01]
1800.92 =   [86* 18.01, 6* 42.01]
...
1800.35 =   [5* 18.01, 3* 42.01, 9* 176.03]
1800.33 =   [2* 42.01, 1* 132.04, 9* 176.03]
1800.35 =   [3* 18.01, 1* 162.05, 9* 176.03]
988 results found!
Took 0.656 milliseconds

另外,只是为了好玩,这些是您问题的确切解决方案。其中有 5 个。

1800.71 =   [12* 18.01, 1* 42.01, 2* 162.05, 6* 203.08]
1800.71 =   [13* 18.01, 2* 42.01, 2* 132.04, 6* 203.08]
1800.71 =   [16* 18.01, 7* 42.01, 6* 203.08]
1800.71 =   [52* 18.01, 1* 42.01, 1* 132.04, 1* 162.05, 3* 176.03]
1800.71 =   [54* 18.01, 4* 42.01, 1* 132.04, 3* 176.03]

【讨论】:

  • 很高兴看到时间比较。我用 JavaScript 写了一个解决方案,同样输出 988 个解决方案,运行时间约为 55ms:codepen.io/anon/pen/mZpwOp
  • @RickHitchcock 不错!我没有意识到javascript现在这么快。对我来说,花了 66 毫秒(Chrome),145 毫秒(Firefox)。
  • 我现在已经将我的 JavaScript 解决方案缩短到大约 20 毫秒。
  • 考虑到所有涉及的字符串转换,我对这种速度印象深刻,做得好
【解决方案2】:

与现有优秀答案相同的另一个答案。我发现使用范围而不是目标 + 容差更简单,并使用简单(未优化)的递归解决方案,这似乎足够快,可以找到约 1000 个用例的答案。

更改为使用 generators/yield 或优化单值情况并没有改变所有结果所花费的时间,但如果您有管道,您可能会发现它很有用。

def fuzzy_coins(vals, lower, upper):
    '''
    vals: [Positive]
    lower: Positive
    upper: Positive
    return: [[Int]]
    Returns a list of coefficients for vals such that the dot
    product of vals and return falls between lower and upper.
    '''
    ret = []
    if not vals:
        if lower <= 0 <= upper:
            ret.append(())
    else:
        val = vals[-1]
        for i in xrange(int(upper / val) + 1):
            for sub in fuzzy_coins(vals[:-1], lower, upper):
                ret.append(sub + (i,))
            lower -= val
            upper -= val
    return ret

即便如此,这在 python 2.7 和 3.6 中需要大约 100 毫秒

[('1800.33', (0, 2, 1, 0, 0, 9)),
 ('1800.35', (3, 0, 0, 1, 0, 9)),
 ('1800.35', (5, 3, 0, 0, 0, 9)),
 ('1800.38', (0, 10, 0, 2, 0, 6)),
 ('1800.38', (1, 11, 2, 0, 0, 6)),
...
 ('1800.92', (86, 6, 0, 0, 0, 0)),
 ('1800.94', (88, 2, 1, 0, 0, 0)),
 ('1800.96', (91, 0, 0, 1, 0, 0)),
 ('1800.96', (93, 3, 0, 0, 0, 0)),
 ('1801.00', (100, 0, 0, 0, 0, 0))]
Took 0.10885s to get 988 results

例如用法:

from __future__ import print_function
import pprint
import time


def main():
    vals = [18.01, 42.01, 132.04, 162.05, 203.08, 176.03]
    target = 1800.71
    fuzz = .5

    lower = target - fuzz
    upper = target + fuzz
    start = time.time()
    coefs = fuzzy_coins(vals, lower, upper)
    end = time.time()
    pprint.pprint(sorted(
        ('%.2f' % sum(c * v for c, v in zip(coef, vals)), coef)
        for coef in coefs
    ))
    print('Took %.5fs to get %d results' % (end - start, len(coefs)))

【讨论】:

  • 我知道你在那里做了什么。很聪明!来试试这个版本在 rust 中运行的速度有多快 :)
  • 原来,它们的速度完全一样。唯一真正起作用的变化是 vals[:-1] 而不是 vals[1:],这使速度减半。所以我的 rust 实现现在是 2.1ms :)
  • @Finomnis 不错!很高兴看到 haskell 习惯仍然有一点好处 =)
  • 将时间缩短到 12 毫秒,Rust 版本 0.7 毫秒 ;)
  • 干得好!你可能会放弃 math.floor ,因为无论如何你都会对它进行 int-cast ,也许还有 ceil 。对我来说,我并没有看到单一案例的加速,但我一定错过了一些东西。
【解决方案3】:

我实现了一个递归来获取输入列表中的所有值组合,该组合的总和在阈值内。输出在列表out 中(总和和组合列表的元组。我没有将其全部打印出来,因为它很大)。

lst = [18.01, 42.01, 132.04, 162.05, 203.08, 176.03]
target = 1800.71

def find_combination(lst, target, current_values=[], curr_index=0, threshold=0.5):
    s = sum(current_values)

    if abs(s - target) <= threshold:
        yield s, tuple(current_values)

    elif s - target < 0:
        for i in range(curr_index, len(lst)):
            yield from find_combination(lst, target, current_values + [lst[i]], i)

    elif s - target > 0:
        curr_index += 1
        if curr_index > len(lst) - 1:
            return

        yield from find_combination(lst, target, current_values[:-1] + [lst[curr_index]], curr_index)

out = []
for v in find_combination(sorted(lst, reverse=True), target):
    out.append(v)

out = [*set(out)]

print('Number of combinations: {}'.format(len(out)))

## to print the output:
# for (s, c) in sorted(out, key=lambda k: k[1]):
#   print(s, c)

打印:

Number of combinations: 988

编辑:过滤掉重复项。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-04
    相关资源
    最近更新 更多