PHP: найдите два или более чисел из списка чисел, которые складываются в заданную сумму

Я пытаюсь создать небольшой PHP скрипт, который может сделать мою жизнь немного легче. В принципе, у меня будет 21 текстовое поле на странице, где я собираюсь ввести 20 разных чисел. В последнем поле я введу число, назовем его общей суммой. Все, что я хочу, чтобы скрипт сделал, это указать, какие числа из 20 полей будут добавлены до общей суммы.

пример:

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-кодом для этого, будет очень признателен.

7 ответов


если вы посмотрите на алгоритм oezis один недостаток сразу ясен: он тратит очень много времени на суммирование чисел, которые, как уже известно, не работают. (Например, если 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);

к сожалению для добавления нового ответа, но это совершенно новое решение для решения всех проблем жизни, Вселенной и всего такого...:

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, все решения возвращаются (для тестирования я использовал те же числа, что и заф в своем посте - в этом случае есть 338 решений, найденных в ~10sec на моя машина.)

изменить: если вы получите все, вы получите результаты, упорядоченные по которым "лучше" - без этого вы получите только первое найденное решение (которое не обязательно является лучшим).

EDIT2: чтобы избежать необходимости объяснений, я прокомментировал основные части Кодекса . если кому-то нужно больше объяснений, пожалуйста, спросите


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

это эффективно и займет меньше времени выполнения.


следующий метод даст вам ответ... почти все время. Увеличьте переменную итераций на свой вкус.

<?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

вероятно, неэффективное, но простое решение с обратным отслеживанием

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
)

Я не думаю, что ответ не так прост, как Ник говорил. давайте ay у вас есть следующие номера:

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... я думаю, что единственное решение-попробовать все возможное комбинация числа и остановить, если amaunt вы ищете найден (или реально вычислить все, если есть различные решения и сохранить, какой из них использовал наименьшие числа).

EDIT: реальный расчет всех возможных комбинаций 21 числа будет в конечном итоге в реальных, реальных, реальных вычислениях-поэтому должно быть любое "интеллектуальное" решение для добавления чисел в специальном порядке (lik, что один в Niks post - с некоторыми улучшениями, возможно, это приведет нас к надежному решение)


не зная, является ли это домашним заданием или нет, я могу дать вам некоторый псевдо-код в качестве подсказки для возможного решения, обратите внимание, что решение не очень эффективно, скорее демонстрация.

подсказка:

сравните каждое значение поля со всем значением поля и на каждой итерации проверьте, равна ли их сумма 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

обновление:

что вы, кажется, является подмножество сумма проблема, приведенный в ссылке Вики псевдокод для алгоритма, который может помочь указать вам в правильном направлении.