从数组值组合计算最接近的匹配项

时间:2012-03-29 11:59:35

标签: php

我有一系列零件长度,例如: -

array(150, 180, 270);

然后我有一个衡量标准($a = 440)

我需要计算长度大于$a的两个最接近的可能组合,而无需手动编写数百种可能的组合来完成它。

所以:

150
180
270

150 + 150
150 + 180
150 + 270

180 + 180
180 + 270

270 + 270

150 + 150 + 150
150 + 150 + 180

..等等。

这需要运行一定次数,而不是仅仅找到前两个匹配并停止,因为150 + 150 + 150将是与$a更接近匹配比270 + 270但可能会追赶。

编辑:我还需要存储构成匹配的部分组合,最好是存储在数组中。

我希望我能够很好地解释这一点,以便有人理解。

4 个答案:

答案 0 :(得分:1)

以下代码是强力的,只测试2个值的可能组合,所以我知道它不完整。但是,这是一个开始。

更新:请参阅下面的其他答案,以获得更好的解决方案,该解决方案适用于任何可能的组合,而不仅仅是2,并且已经过优化。

<?php

    echo "<html><head><title>Test Array Sums</title></head><body>";
    $testarray = array(2, 5, 9, 78, 332);
    $target_value = 10;
    $closest1 = 0;
    $closest2 = 0;
    $closest_sum = 0;
    $closest_difference = 0;
    $first_time_in_loop = TRUE;
    foreach ($testarray AS $entry1)
    {
        foreach ($testarray AS $entry2)
        {
            if ($first_time_in_loop)
            {
                $first_time_in_loop = FALSE;
                $closest1 = $entry1;
                $closest2 = $entry2;
                $closest_sum = $closest1 + $closest2;
                $closest_difference = abs($target_value - $closest_sum);
            }

            $test_sum = $entry1 + $entry2;
            if (abs($test_sum - $target_value) < $closest_difference)
            {
                if ($test_sum - $target_value >= 0)
                {
                    // Definitely the best so far
                    $closest1 = $entry1;
                    $closest2 = $entry2;
                    $closest_sum = $closest1 + $closest2;
                    $closest_difference = abs($closest_sum - $target_value);
                }
                else if ($closest_sum - $target_value < 0)
                {
                    // The sum isn't big enough, but neither was the previous best option
                    // and at least this is closer
                    $closest1 = $entry1;
                    $closest2 = $entry2;
                    $closest_sum = $closest1 + $closest2;
                    $closest_difference = abs($closest_sum - $target_value);
                }
            }
            else
            {
                if ($closest_sum - $target_value < 0 && $test_sum - $target_value >= 0)
                {
                    // $test_value is farther away from the target than the previous best option,
                    // but at least it's bigger than the target value (the previous best option wasn't)
                    $closest1 = $entry1;
                    $closest2 = $entry2;
                    $closest_sum = $closest1 + $closest2;
                    $closest_difference = abs($closest_sum - $target_value);
                }
            }
        }
    }
    echo "Best pair: " . $closest1 . ", " . $closest2 . "<br />";
    echo "</body></html>";
?>

您可以将测试值的总数限制为3 - 或更大的数字 - 或者您是否真的需要将其扩展到所有可能的组合(即,如果4 + 4 + 5 + 4 + 4 + 5 + 3 + 5 + 4 + 5 + 3 + 4比26 + 26更接近你需要找到它吗?)

如果您可以将测试的数量限制为5,那么您可以将上面的循环扩展到最多可以处理5个选项。否则,需要编写更复杂的循环。

答案 1 :(得分:1)

此代码计算出$ a之上最接近的组合,以及之后的下一个最接近的组合。它会删除重复项以加快速度。它不是超级优化的,但初步测试显示它并不太糟糕,取决于$ a的初始值不大。

<?php
/* value in cm */
$a = 1020;
$partLengths = array(150, 180, 270);
$closestValue = array();
$secondClosest = array();
$currentCombinations = array(
    array(
        'total' => 150,
        'combination' => array(150)
    ),
    array(
        'total' => 180,
        'combination' => array(180)
    ),
    array(
        'total' => 270,
        'combination' => array(270)
    )
);

function getCombinations(&$currentCombinations, $partLengths,$a, &$closestValue, &$secondClosest) { 
    $tmpCombinations = $currentCombinations;
    static $secondMatch = true;
    for ($x=0;$x<count($partLengths);$x++) {
        for ($y=0;$y<count($tmpCombinations);$y++) {
            $newCombination = $tmpCombinations[$y]['combination'];
            $newCombination[] = $partLengths[$x];
            $newCombinationTotal = array_sum($newCombination);
            sort($newCombination);

            if (!combinationExists($currentCombinations, $newCombination, $newCombinationTotal)) {
                $currentCombinations[] = array('total' => $newCombinationTotal, 'combination' => $newCombination);
            }

            if ($closestValue['total'] < $a) {
                $oldGap = $a - $closestValue['total'];
                $newGap = $a - $newCombinationTotal;
                $newGap = ($newGap < 0) ? 0 - $newGap : $newGap;

                if ($newGap < $oldGap) {
                    $secondClosest = $closestValue;
                    $closestValue['total'] = $newCombinationTotal;
                    $closestValue['combination'] = $newCombination;
                }
            } else {
                $oldGap = $a - $secondClosest['total'];
                $newGap = $a - $newCombinationTotal;
                $oldGap = ($oldGap < 0) ? 0 - $oldGap : $oldGap;
                $newGap = ($newGap < 0) ? 0 - $newGap : $newGap;

                if ($newCombinationTotal > $a && $newCombinationTotal > $closestValue['total']) {
                    if ($secondMatch || $newGap < $oldGap) {
                        $secondMatch = false;
                        $secondClosest['total'] = $newCombinationTotal;
                        $secondClosest['combination'] = $newCombination;
                    }
                }
            }
        }
    }
}
function combinationExists(&$currentCombinations, $newCombination, $newCombinationTotal) {
    foreach ($currentCombinations as $currentCombination) {
        if ($currentCombination['total'] != $newCombinationTotal && $currentCombination['combination'] != $newCombination) {
            return false;
        }
    }
    return false;
}

while ($secondClosest['total'] <= $a) {
    getCombinations($currentCombinations, $partLengths, $a, $closestValue, $secondClosest);
}

var_dump($closestValue);
var_dump($secondClosest);
?>

如果速度确实成为一个问题,另一个建议是预先生成所有组合并将它们保存在您可以轻松访问的某种哈希/数据库/等中。

答案 2 :(得分:1)

改进我以前的答案,这是一个可以测试任意数量的条目的版本,最多可以达到最大数量。

更新 :(已添加优化;请参阅下面的评论)

例如,如果所需的值为15,且列表为(1, 17, 20),则最佳选择为1+1+1+1+1+1+1+1+1+1+1+1+1+1+1,因此您必须允许$max_loops, ,至少15才能找到匹配项 - 即使列表中只有3个值! (1, 133, 138)更糟糕,其中所需的值是130。在这种情况下,您需要 130 递归!你可以看到这可能是一场优化的噩梦。但是,下面的算法可以正常工作并且相当好。

<?php

    echo "<html><head><title>Test Array Sums</title></head><body>";

    $testarray = array(1, 3, 6);
    $target_value = 10;

    $current_closest_sum = 0;
    $current_closest_difference = 0;
    $first_time_in_loop = TRUE;

    $max_loops = 10;
    $current_loop = 0;

    $best_set = array();
    $current_set = array();

    $sums_already_evaluated = array();

    function nestedLoop($current_test = 0)
    {
        global $testarray, $target_value, $current_closest_sum, $current_closest_difference, $first_time_in_loop, $max_loops, $current_loop, $best_set, $current_set, $sums_already_evaluated;

        ++$current_loop;
        foreach ($testarray AS $entry)
        {
            $current_set_temp = $current_set;
            $current_set[] = $entry;
            if ($first_time_in_loop)
            {
                $first_time_in_loop = FALSE;
                $current_closest_sum = $entry + $current_test;
                $current_closest_difference = abs($target_value - $current_closest_sum);
                $best_set[] = $entry;
            }

            $test_sum = $entry + $current_test;

            if (in_array($test_sum, $sums_already_evaluated))
            {
                // no need to test a sum that has already been tested
                $current_set = $current_set_temp;
                continue;
            }
            $sums_already_evaluated[] = $test_sum;

            if ($test_sum > $target_value && $current_closest_sum > $target_value && $test_sum >= $current_closest_sum)
            {
                // No need to evaluate a sum that is certainly worse even by itself
                $current_set = $current_set_temp;
                continue;
            }

            $set_best = FALSE;
            if (abs($test_sum - $target_value) < $current_closest_difference)
            {
                if ($test_sum - $target_value >= 0)
                {
                    // Definitely the best so far
                    $set_best = TRUE;
                }
                else if ($current_closest_sum - $target_value < 0)
                {
                    // The sum isn't big enough, but neither was the previous best option
                    // and at least this is closer
                    $set_best = TRUE;
                }
            }
            else
            {
                if ($current_closest_sum - $target_value < 0 && $test_sum - $target_value >= 0)
                {
                    // $test_value is farther away from the target than the previous best option,
                    // but at least it's bigger than the target value (the previous best option wasn't)
                    $set_best = TRUE;
                }
            }
            if ($set_best)
            {
                $current_closest_sum = $test_sum;
                $current_closest_difference = abs($current_closest_sum - $target_value);
                $best_set = $current_set;
            }
            if ($current_loop < $max_loops)
            {
                if ($test_sum - $target_value < 0)
                {
                    nestedLoop($test_sum);
                }
            }
            $current_set = $current_set_temp;
        }
        --$current_loop;
    }

    // make array unique
    $testarray = array_unique($testarray);
    rsort($testarray, SORT_NUMERIC);

    // Enter the recursion
    nestedLoop();

    echo "Best set: ";
    foreach ($best_set AS $best_set_entry)
    {
        echo $best_set_entry . " ";
    }
    echo "<br />";
    echo "</body></html>";
?>

UPDATE :我添加了两个似乎有用的小优化,并避免了内存过载或散列表查找。他们是:

(1)跟踪所有先前评估的总和,不再评估它们。

(2)如果一笔金额(本身)已经比之前的测试更差,则跳过任何进一步的测试。

我认为,通过这两种优化,算法可以很好地适用于您的实际情况。

以前的评论,现在有些不相称

我之前的评论,在下面,有点没有实际意义,因为上述两个优化似乎确实很有效。但无论如何我都会提出意见。

不幸的是,如上所述,上述循环非常不优化。必须通过避免重复测试(以及其他优化)来优化以便在现实情况下工作。但是,它演示了一种有效的算法。

请注意,这是一个数学上的复杂区域。在一种情况下,各种优化可能有所帮助,但在另一种情因此,为了使上述算法有效工作,您需要讨论实际使用场景 - 部件列表中的最大长度是否有限制?长度范围是多少?零件清单和其他更细微的功能。期望的目标虽然微妙,但可能会对如何优化算法产生重大影响。

这是“理论”问题不足以产生所需解决方案的情况,因为优化是非常重要的。因此,提出优化建议并不是特别有用。

例如,伦纳德的优化(通过保存先前测试的所有组合避免重复)适用于小型集合,但是对于较大的集合,内存使用会爆炸(正如他所指出的)。这不是一个简单的问题。

(代码编辑~2小时后处理可能错过的组合,因为将递归限制为一定数量的递归 - 通过最初将数组从高到低排序)

答案 3 :(得分:1)

由于这是一个资源非常繁重的脚本,我认为最好先选择生成选项,然后使用该数据创建一个变量/ object / sql脚本来永久存储数据。例如,做一些像

这样的事情
SELECT * FROM combination_total WHERE size > YOUR_SIZE ORDER BY size ASC LIMIT 2;

我所拥有的新脚本是类似的,但它只是生成一个包含所有组合的数组而没有任何重复。似乎很快。请注意$ maxLength变量,该变量当前设置为2000,可以使用您自己的最大可能大小进行修改。

<?php
$partLengths = array(150, 180, 270);
$currentCombinations = array(
    array(
        'total' => 150,
        'combination' => array(150)
    ),
    array(
        'total' => 180,
        'combination' => array(180)
    ),
    array(
        'total' => 270,
        'combination' => array(270)
    )
);
$maxLength = 2000;
$largestSize = 0;

function generateCombination() {
    global $currentCombinations, $largestSize, $partLengths;
    $tmpCombinations = $currentCombinations;
    foreach ($tmpCombinations as $combination) {
        foreach ($partLengths as $partLength) {
            $newCombination = $combination['combination'];
            $newCombination[] = $partLength;
            sort($newCombination);

            $newCombinationTotal = array_sum($newCombination);

            if (!combinationExists($newCombination)) {
                $currentCombinations[] = array(
                        'total' => $newCombinationTotal,
                        'combination' => $newCombination
                );
            }

            $largestSize = ($newCombinationTotal > $largestSize) ? $newCombinationTotal : $largestSize;
        }
    }
}

function combinationExists($combination) {
    global $currentCombinations;
    foreach ($currentCombinations as $currentCombination) {
        if ($combination == $currentCombination['combination']) {
            return true;
        }
    }
    return false;
}

while ($largestSize < $maxLength) {
    generateCombination();
}

// here you can use $currentCombinations to generate sql/object/etc
var_dump($currentCombinations);
?>