Find sum of values in array

Asked

Viewed 1,816 times

5

I need to develop an algorithm that finds a specific value of the sum of some elements of an array. Ex: I have an array with 50 different values and have a given value "X".

I know that the sum of the combination of some elements of this array results in this value, what I need to know is which are the elements within this list that gives this sum. I cannot use combination algorithms because above 9 elements already gives exceeded memory limit and I have arrays much larger than this to calculate.

The code I have so far is this:

/**
 * Class Teste
 */
class Teste
{
    static $values  = array(15.92, 53.27, 244.28, 388.46, 3.14, 2.92, 18.22, 4.03);
    static $include = array();
//    static         $expected = 712.02;
    static         $expected = 297.55;
    static         $ok       = false;
    private static $_instance;

    /**
     * @return static
     */
    public static function instance()
    {
        return static::$_instance = static::$_instance ?: new static();
    }

    public function __construct()
    {
        while (!static::$ok) {
            reset(static::$include);
            static::removeItem(key(static::$include));
            static::calc();
        }
        var_export(static::sum());
    }

    public static function calc()
    {
        foreach (static::$values as $k => $v) {
            var_export(static::$include);
            if (round(static::$expected, 2) == round(static::sum(), 2)) {
                static::$ok = true;
                break;
            } else if (static::$expected > static::sum()) {
                static::addItem($k);
            }
            if (round(static::$expected, 2) < round(static::sum(), 2)) {
                static::removeItem($k);
            }
        }
    }

    public static function addItem($k)
    {
        if (!array_key_exists($k, static::$include)) {
            static::$include[$k] = round(static::$values[$k], 2);
        }
    }

    public static function removeItem($k)
    {
        unset(static::$include[$k]);
    }

    public static function sum()
    {
        return round(array_sum(static::$include), 2);
    }
}

Teste::instance();
  • The problem is cool, but in php it’s really challenging. In Javascript, I would create a setInterval cycle to run an algorithm in a controlled way, without bursting memory. In php, I think you’ll need to see the creation of http://au2.php.net/manual/en/intro.pthreads.php threads or some asynchronous way of working, otherwise any algorithm will pop.

  • @user6492 uses an iterative solution that then the maximum memory you will consume is about the same size as the final response. What consumes the most memory in such a problem is recursion.

3 answers

5

Optimized solution:

I had posted a reasonable solution (can be seen in the edits history of the reply), but I ended up researching a little more about this type of combination, and I came to this code:

<?php
   static $values  = array( 15.92, 53.27, 244.28, 388.46, 3.14, 2.92, 18.22, 4.03 );
   static $expected = 297.55;
   static $precision = 100; /* para não ter problemas com ponto flutuante */

   $target = floor( $expected * $precision );
   $len = count( $values );
   for( $i = 1; $i < pow( 2, $len ); $i++ ) {
      $soma = 0;
      $set = array();
      for( $j = 0; $j < $len; $j++ ) {
         if( 1 << $j & $i ) {
            $set[] = $j;
            $soma += floor( $values[$j] * $precision );
         }
      }
      if( $soma == $target ) {
         // Estamos exibindo na tela apenas como demonstração. Se preferir pode armazenar.
         foreach( $set as $pos ) echo "[$pos]{$values[$pos]} ";
         echo " = $expected<br>\n";
      }
   }
?>

Thanks to a excellent algorithm optimized for unique combinations I found, it was possible to make code much faster and with little memory usage.

The solutions I tried earlier exceeded the 5-second timeout on IDEONE, even with only eight values in the array. This one is working extremely well at this time with that amount of data.

See working on IDEONE.

  • It also serves to find all combinations like the previous one (although send directly to the output)?

  • @Brunoaugusto He does not repeat results out of order as the previous one. Since the proposed goal is to find which ones give the desired sum, it does not make much sense to find the same result in different orders (see note in edition history, version 3 and following). The search for improvement in the algorithm was mainly to remedy these cases.

  • This I understood, it was more out of curiosity even because a time ago I participated in a challenge ~~to find the combines;'oes among all n[umeros of a given matrix (regardless of quantity), but all implements;'oes were very slow with PHP. I wanted to know if this yours [is better.

  • @Bruno I edited the answer because it has a bigger performance than the other paths I tried, by the same mathematical question. Technically it is not so much "fast" in the sense of processing speed, but the "cat jump" of this is that the binary mask search eliminates many sets that would be generated simply to be discarded in other implementations, That’s where the real time comes from. I suggest a read on link to the original algorithm, there’s a legal explanation there.

4

I’ve never programmed in PHP in my life. I’ll leave an algorithm here in javascript - I ask someone to create a new answer by converting to PHP (will have my upvote).

First you take all possible combinations (disregarding order). I had done something similar before in another answer, but I did without recursion here not to burst your stack ;)

var ar = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var resultado = {
    "1": {}
};
for (var i = 0; i < ar.length; i++) {
    resultado["1"][ar[i] + ""] = [ar[i]];
}

var tamanhoMaximo = ar.length;

for (var tamanho = 2; tamanho <= tamanhoMaximo; tamanho++) {
    var tamanhoAtual = resultado[tamanho + ""] = {};
    var tamanhoAnterior = resultado[(tamanho - 1) + ""];
    for (var chave in tamanhoAnterior) {
        var tempAr = tamanhoAnterior[chave];
        for (var i = 0; i < ar.length; i++) {
            if (tempAr.indexOf(ar[i]) == -1) {
                var novoAr = tempAr.slice();
                novoAr.push(ar[i]);
                novoAr.sort();
                tamanhoAtual[novoAr.join(",")] = novoAr;
            }
        }
    }
}
resultado;

Now just scroll through the map and see which combinations give your sum.

function encontraCombinacoes (mapa, procurado) {
    for (var chave in mapa) {
        for (var subchave in mapa[chave]) {
            var array = mapa[chave][subchave];
            var soma = 0;
            for (var i = 0; i < array.length; i++) {
                soma += array[i];
            }
            if (soma == procurado) {
                console.log(subchave);
            }
        }
    }
}
encontraCombinacoes(resultado, 6); // Só de exemplo

Note that you could already check the sums in the first code. What I did here accumulates combinations that can pass the value you are looking for. You can make the algorithm more efficient by counting sums while assembling combinations.

When someone converts this to PHP, I can delete this answer.

Edit: there’s an error in the algorithm and it doesn’t pick up all the combinations (about 20% are missing). As soon as I can fix it - or someone else can find the bug and fix it before!. Corrected!

  • Thanks, I’ll convert and then put here.

  • 1

    There are still errors in this code! I’ll fix it later!

  • Will it work with 50 items? You did what I had in mind, but instead of going, I would use one of the ... while with an external counter and execution via setInterval, to ensure that it does not cross the browser.

  • It will work with 50 items yes, but it can be slow as hell.

  • Because it uses this new var = tempAr.Slice();

  • @Daniel In javascript, the function slice make a copy of the array. I use a copy to leave the already mounted arrays intact.

Show 1 more comment

0


This solution was the most effective for arrays with many elements.

$notas = [
  999 => 8,
  456 => 4,
  789 => 5,
  123 => 2,
];

asort($notas);

function remove_maiores_que($total, $numeros) {
    return array_filter($numeros, function($n) use ($total) {
        return $n <= $total;
    });
}

function find_soma($total, $numeros) {
    if ($total <= 0) return array();

    // primeiro remove os numeros maiores que o total buscado
    $numeros = remove_maiores_que($total, $numeros);

    $sum = array_sum($numeros);

    // se a soma de todos elementos do array for inferior ao total, não adianta procurar
    if ($sum < $total) {
        return array();
    }

    // já achou o array
    if ($sum == $total) {
        return $numeros;
    }

    // remove o maior e procura soma do restante
    while( end($numeros) ) { // enquanto tiver numeros no array
        // remove o maior e sua respectiva chave
        $key = key($numeros);
        $n = array_pop($numeros);

        // verifica se o numero já é igual ao total
        if ($n == $total) {
            return array($key => $n);
        }

        // no array que sobrou, procura pela diferença do total e o número maior removido acima
        $sub_total = $total - $n;

        $soma = find_soma($sub_total, $numeros);

        // se a soma não for vazia, então encontrou a sub soma
        if ( ! empty($soma) ) {
            // adiciona o numero atual no final da soma e retorna
            $soma[$key] = $n;
            return $soma;
        }
    }

    // retorna array vazio indicando que não encontrou nenhuma soma
    return array();
}

find_soma(6, $notas);

Browser other questions tagged

You are not signed in. Login or sign up in order to post.