【问题标题】:PHP: find two or more numbers from a list of numbers that add up towards a given amountPHP:从加起来给定数量的数字列表中找到两个或多个数字
【发布时间】:2017-09-25 14:57:18
【问题描述】:

我正在尝试创建一个可以让我的生活更轻松的小 php 脚本。 基本上,我将在一个页面上有 21 个文本字段,我将在其中输入 20 个不同的数字。在最后一个字段中,我将输入一个数字,我们称之为 TOTAL AMOUNT。我想要脚本做的只是指出 20 个字段中的哪些数字加起来会达到 TOTAL AMOUNT。

例子:

field1 = 25.23
field2 = 34.45
field3 = 56.67
field4 = 63.54
field5 = 87.54
....
field20 = 4.2

Total Amount = 81.90

输出:field1 + fields3 = 81.90

有些字段的值可能为 0,因为有时我只需要输入 5-15 个字段,最大值为 20。

如果有人能帮助我解决这个问题的 php 代码,将不胜感激。

【问题讨论】:

  • 让您的生活更轻松还是让大学项目更轻松? ;)
  • 如果您找到了问题的答案,请通过单击该答案左侧的复选框大纲来接受这一点 - 您还没有对其他问题执行此操作,而且有时人们可能会赢如果这样做没有得到回报,那将无济于事。
  • 您在此期间尝试过我的功能吗?结果是什么?很高兴听到它是否对您有所帮助...
  • 有点让我想起了xkcd.com/287的简化版

标签: php sum


【解决方案1】:

如果你看一下oezis algorithm,一个缺点就很明显了:它会花费大量时间来汇总已知无效的数字。 (例如,如果 1 + 2 已经太大,那么尝试 1 + 2 + 3、1 + 2 + 3 + 4、1 + 2 + 3 + 4 + 5、... 也没有任何意义.)

因此我写了一个改进的版本。它不使用一点魔法,它使一切都是手动的。一个缺点是,它需要对输入值进行排序(使用rsort)。但这应该不是什么大问题;)

function array_sum_parts($vals, $sum){
    $solutions = array();
    $pos = array(0 => count($vals) - 1);
    $lastPosIndex = 0;
    $currentPos = $pos[0];
    $currentSum = 0;
    while (true) {
        $currentSum += $vals[$currentPos];

        if ($currentSum < $sum && $currentPos != 0) {
            $pos[++$lastPosIndex] = --$currentPos;
        } else {
            if ($currentSum == $sum) {
                $solutions[] = array_slice($pos, 0, $lastPosIndex + 1);
            }

            if ($lastPosIndex == 0) {
                break;
            }

            $currentSum -= $vals[$currentPos] + $vals[1 + $currentPos = --$pos[--$lastPosIndex]];
        }
    }

    return $solutions;
}

oezis 测试程序的修改版本(见末尾)输出:

possibilities: 540
took: 3.0897309780121

所以执行只需要 3.1 秒,而 oezis 代码在我的机器上执行 65 秒(是的,我的机器很慢)。这比 快 20 倍

此外,您可能会注意到,我的代码找到了540 而不是338 的可能性。这是因为我调整了测试程序以使用整数而不是浮点数。 直接浮点比较很少是正确的做法,这是一个很好的例子:您有时会得到59.959999999999 而不是59.96,因此不会计算匹配。所以,如果我用整数运行 oezis 代码,它也会找到 540 种可能性;)

测试程序:

// Inputs
$n = array();
$n[0]  = 6.56;
$n[1]  = 8.99;
$n[2]  = 1.45;
$n[3]  = 4.83;
$n[4]  = 8.16;
$n[5]  = 2.53;
$n[6]  = 0.28;
$n[7]  = 9.37;
$n[8]  = 0.34;
$n[9]  = 5.82;
$n[10] = 8.24;
$n[11] = 4.35;
$n[12] = 9.67;
$n[13] = 1.69;
$n[14] = 5.64;
$n[15] = 0.27;
$n[16] = 2.73;
$n[17] = 1.63;
$n[18] = 4.07;
$n[19] = 9.04;
$n[20] = 6.32;

// Convert to Integers
foreach ($n as &$num) {
    $num *= 100;
}
$sum = 57.96 * 100;

// Sort from High to Low
rsort($n);

// Measure time
$start = microtime(true);
echo 'possibilities: ', count($result = array_sum_parts($n, $sum)), '<br />';
echo 'took: ', microtime(true) - $start;

// Check that the result is correct
foreach ($result as $element) {
    $s = 0;
    foreach ($element as $i) {
        $s += $n[$i];
    }
    if ($s != $sum) echo '<br />FAIL!';
}

var_dump($result);

【讨论】:

  • +1 真的很棒!我在我的代码中也知道这个问题并想修改它,但从来没有找到时间和空间来做这件事……自从我发帖后,splash 就不在线了,所以他甚至不会认出它。还有一个永远不会被正式回答的问题(而这个就是答案)
  • @oezi:回答老问题时总是有风险。但是,嘿,编写算法很有趣,即使你不能指望为它们赢得声誉;)
  • 绝对 - 我在一次无聊到流泪的商业经济学讲座中编写了我的函数 - 这样的问题非常适合这些情况
【解决方案2】:

很抱歉添加了一个新答案,但这是一个全新的解决方案,可以解决生命、宇宙和一切的所有问题……:

function array_sum_parts($n,$t,$all=false){
    $count_n = count($n); // how much fields are in that array?
    $count = pow(2,$count_n); // we need to do 2^fields calculations to test all possibilities

    # now i want to look at every number from 1 to $count, where the number is representing
    # the array and add up all array-elements which are at positions where my actual number
    # has a 1-bit
    # EXAMPLE:
    # $i = 1  in binary mode 1 = 01  i'll use ony the first array-element
    # $i = 10  in binary mode 10 = 1010  ill use the secont and the fourth array-element
    # and so on... the number of 1-bits is the amount of numbers used in that try

    for($i=1;$i<=$count;$i++){ // start calculating all possibilities
        $total=0; // sum of this try
        $anzahl=0; // counter for 1-bits in this try
        $k = $i; // store $i to another variable which can be changed during the loop
        for($j=0;$j<$count_n;$j++){ // loop trough array-elemnts
            $total+=($k%2)*$n[$j]; // add up if the corresponding bit of $i is 1
            $anzahl+=($k%2); // add up the number of 1-bits
            $k=$k>>1; //bit-shift to the left for looking at the next bit in the next loop
        }
        if($total==$t){
            $loesung[$i] = $anzahl; // if sum of this try is the sum we are looking for, save this to an array (whith the number of 1-bits for sorting)
            if(!$all){
                break; // if we're not looking for all solutions, make a break because the first one was found
            }
        }
    }

    asort($loesung); // sort all solutions by the amount of numbers used


    // formating the solutions to getting back the original array-keys (which shoud be the return-value)
    foreach($loesung as $val=>$anzahl){
        $bit = strrev(decbin($val));
        $total=0;
        $ret_this = array();
        for($j=0;$j<=strlen($bit);$j++){
            if($bit[$j]=='1'){
                $ret_this[] = $j;
            }   
        }
        $ret[]=$ret_this;
    }

    return $ret;
}

// Inputs
$n[0]=6.56;
$n[1]=8.99;
$n[2]=1.45;
$n[3]=4.83;
$n[4]=8.16;
$n[5]=2.53;
$n[6]=0.28;
$n[7]=9.37;
$n[8]=0.34;
$n[9]=5.82;
$n[10]=8.24;
$n[11]=4.35;
$n[12]=9.67;
$n[13]=1.69;
$n[14]=5.64;
$n[15]=0.27;
$n[16]=2.73;
$n[17]=1.63;
$n[18]=4.07;
$n[19]=9.04;
$n[20]=6.32;

// Output
$t=57.96;

var_dump(array_sum_parts($n,$t)); //returns one possible solution (fuc*** fast)

var_dump(array_sum_parts($n,$t,true)); // returns all possible solution (relatively fast when you think of all the needet calculations)

如果您不使用第三个参数,它将返回最佳(使用最少的数字)解决方案作为数组(输入数组的键) - 如果您将第三个参数设置为 true, ALL返回解决方案(为了测试,我在他的帖子中使用了与 zaf 相同的数字 - 在这种情况下有 338 个解决方案,在我的机器上大约 10 秒内找到)。

编辑: 如果你得到了所有,你会得到“最好”的排序结果 - 没有这个,你只会得到第一个找到的解决方案(不一定是最好的)。

编辑2: 为了满足一些解释的愿望,我评论了代码的基本部分。如果有人需要更多解释,请询问

【讨论】:

  • 谢谢 oezi,今晚我会试一试。将发布结果。克里斯蒂安。
  • 在我+1你之前,你能解释一下这个算法吗?
  • 我评论了上面的代码,希望这能帮助你理解 - 如果没有,请再次询问。
  • 对于总计算,它不是 20 (20!) 的阶乘吗?等一下,我明白你在做什么。好的。
  • 很高兴看到 stackoverflow 上有多少德国用户 :D 我看到许多带有 varnames 的代码 sn-ps,例如 $loesung$anzahl。 @oezi:您应该将 如此变慢。我可以理解我的实现要快两到三倍,因为它放弃了一些检查,但我没想到它会快大约 20 倍。
【解决方案3】:
1. Check and eliminate fields values more than 21st field

2. Check highest of the remaining, Add smallest, 

3. if its greater than 21st eliminate highest (iterate this process)

   4. If lower: Highest + second Lowest, if equal show result.

   5. if higher go to step 7

   6. if lower go to step 4

7. if its lower than add second lowest, go to step 3.

8. if its equal show result

这是高效的,并且会花费更少的执行时间。

【讨论】:

  • 也许效率不高,但我认为它不会得到正确的结果。请参阅我的帖子以获得解释 - 如果我理解错误,请纠正我。
【解决方案4】:

以下方法会给你一个答案......几乎所有的时间。根据您的喜好增加迭代变量。

<?php

// Inputs
$n[1]=8.99;
$n[2]=1.45;
$n[3]=4.83;
$n[4]=8.16;
$n[5]=2.53;
$n[6]=0.28;
$n[7]=9.37;
$n[8]=0.34;
$n[9]=5.82;
$n[10]=8.24;
$n[11]=4.35;
$n[12]=9.67;
$n[13]=1.69;
$n[14]=5.64;
$n[15]=0.27;
$n[16]=2.73;
$n[17]=1.63;
$n[18]=4.07;
$n[19]=9.04;
$n[20]=6.32;

// Output
$t=57.96;

// Let's try to do this a million times randomly
// Relax, thats less than a blink
$iterations=1000000;
while($iterations-->0){
    $z=array_rand($n, mt_rand(2,20));
    $total=0;
    foreach($z as $x) $total+=$n[$x];
    if($total==$t)break;
}

// If we did less than a million times we have an answer
if($iterations>0){
    $total=0;
    foreach($z as $x){
        $total+=$n[$x];
        print("[$x] + ". $n[$x] . " = $total<br/>");
    }
}

?>

一种解决方案:

[1] + 8.99 = 8.99
[4] + 8.16 = 17.15
[5] + 2.53 = 19.68
[6] + 0.28 = 19.96
[8] + 0.34 = 20.3
[10] + 8.24 = 28.54
[11] + 4.35 = 32.89
[13] + 1.69 = 34.58
[14] + 5.64 = 40.22
[15] + 0.27 = 40.49
[16] + 2.73 = 43.22
[17] + 1.63 = 44.85
[18] + 4.07 = 48.92
[19] + 9.04 = 57.96

【讨论】:

  • +1 的解决方案会时不时地工作,但它不是 100% 可靠 - 这真的有点愚蠢。
  • 这种计算方法背后的逻辑远非愚蠢。
  • 但这是一个坏习惯,即(仅在极少数情况下)此脚本最终会没有解决方案,而根本没有找到解决方案 - 因此您必须进行更多迭代才能获得更多可靠(而且在那种情况下可能是脚本找不到任何东西)...根据您用于测试的数字,有 338 个可能的解决方案,如果只有一个怎么办?该脚本有多少次没有找到该解决方案 - 需要多长时间?
  • 这是寻找解决方案的众多方法之一,不是一个坏习惯。当然,在极端情况下,如果只有一个特定的解决方案,那么代码大部分时间都找不到解决方案。这是一个NP完全问题。你知道的方法越多,你就能更好地处理这类问题。无论如何,我无法回答您的具体问题(我不是数学家),也许您可​​以通过mathoverflow.net 询问我刚刚看到您的最新答案 - 让我看看。
  • 我刚刚在我的帖子中评论了代码,也许你可以再看一遍 - 感谢数学溢出的提示......乍一看,它看起来像一个充满绝对怪胎o.0
【解决方案5】:

一个可能效率低下但简单的回溯解决方案

function subset_sums($a, $val, $i = 0) {
    $r = array();
    while($i < count($a)) {
        $v = $a[$i];
        if($v == $val)
            $r[] = $v;
        if($v < $val)
            foreach(subset_sums($a, $val - $v, $i + 1) as $s)
                $r[] = "$v $s";
        $i++;
    }
    return $r;
}

例子

$ns = array(1, 2, 6, 7, 11, 5, 8, 9, 3);
print_r(subset_sums($ns, 11));

结果

Array
(
    [0] => 1 2 5 3
    [1] => 1 2 8
    [2] => 1 7 3
    [3] => 2 6 3
    [4] => 2 9
    [5] => 6 5
    [6] => 11
    [7] => 8 3
)

【讨论】:

    【解决方案6】:

    我认为答案并不像尼克所说的那么简单。假设您有以下数字:

    1 2 3 6 8
    

    寻找金额10

    niks 解决方案会这样做(如果我理解正确的话):

    1*8 = 9 = too low
    adding next lowest (2) = 11 = too high
    

    现在他会删除高数并重新开始取新的最高数

    1*6 = 7 = too low
    adding next lowest (2) = 9 = too low
    adding next lowest (3) = 12 = too high
    

    ... 等等,完美的答案就是 成为8+2 = 10...我认为唯一的解决方案是尝试所有可能的组合 如果找到您正在寻找的数量并停止(或真正计算所有,如果有不同的解决方案并保存哪个使用最少的数字)。

    编辑:真正计算 21 个数字的所有可能组合最终会得到非常、非常、非常多的计算 - 所以必须有任何“智能”解决方案来按特殊顺序添加数字(就像 niks 帖子中的那个 - 与一些改进,也许这会给我们带来一个可靠的解决方案)

    【讨论】:

    • 嘿 oezi,你是礼仪。我做了一些更改,请检查。我希望这对于更多的值会更好,比如你已经取了 1 2 3 6 8,如果有 15 个大于 10 的值,这个算法将一次性取消它们,节省指数检查,因此执行时间更短
    • hm... 我不认为这是正确的,但是(但我不确定)。我现在正在上课,所以我有很多时间尝试你的答案给我的想法 - 我现在会下载 mamp 并尝试一下。
    【解决方案7】:

    在不知道这是否是家庭作业的情况下,我可以给你一些伪代码作为可能的解决方案的提示,注意解决方案不是很有效,更多的是演示。

    提示:

    将每个字段值与所有字段值进行比较,并在每次迭代时检查它们的总和是否等于TOTAL_AMOUNT

    伪代码:

    for i through field  1-20
     for j through field 1-20
       if value of i + value of j == total_amount
            return i and j
    

    更新:

    您似乎拥有的是Subset sum problem,在 Wiki 链接中给出的是该算法的伪代码,它可能有助于为您指明正确的方向。

    【讨论】:

    • 可能超过2个字段
    • 不是一个非常有效的解决方案 - 但适用于正在使用的小型数据集 - 更好的解决方案是将值用作索引键然后搜索 (total_amount-i) 但这会出现重复问题值。
    • @nik,@symcbean:是的,这不是一个非常有效的解决方案,但我想展示一个可能的 OP 比较。现在看到另一个要求是超过 2 个字段,我会尽力在工作中更新我的答案
    • 谢谢你们的回复。只是为了说明这不是学校项目或家庭作业。我试图建立一种方法来从发票列表中查找哪些发票加起来是已转入公司帐户的总金额。我们现在使用的会计软件并不是最聪明的。今晚下班后我会试一试,看看能不能解决问题。
    猜你喜欢
    • 1970-01-01
    • 2020-06-06
    • 2018-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多