10

I've been looking at PHP array permutation / combination questions all day.. and still can't figure it out :/

If I have an array like:

20 //key being 0    
20 //key being 1    
22 //key being 2    
24 //key being 3

I need combinations like:

20, 20, 22 //keys being 0 1 2    
20, 20, 24 //keys being 0 1 3    
20, 22, 24 //keys being 0 2 3
20, 22, 24 //keys being 1 2 3

The code I currently have gives me:

20, 22, 24

because it doesn't want to repeat 20... but that's what I need!

Here is the code I have. it is directly from Php recursion to get all possibilities of strings

function getCombinations($base,$n){

$baselen = count($base);
if($baselen == 0){
    return;
}
    if($n == 1){
        $return = array();
        foreach($base as $b){
            $return[] = array($b);
        }
        return $return;
    }else{
        //get one level lower combinations
        $oneLevelLower = getCombinations($base,$n-1);

        //for every one level lower combinations add one element to them that the last element of a combination is preceeded by the element which follows it in base array if there is none, does not add
        $newCombs = array();

        foreach($oneLevelLower as $oll){

            $lastEl = $oll[$n-2];
            $found = false;
            foreach($base as  $key => $b){
                if($b == $lastEl){
                    $found = true;
                    continue;
                    //last element found

                }
                if($found == true){
                        //add to combinations with last element
                        if($key < $baselen){

                            $tmp = $oll;
                            $newCombination = array_slice($tmp,0);
                            $newCombination[]=$b;
                            $newCombs[] = array_slice($newCombination,0);
                        }

                }
            }

        }

    }

    return $newCombs;


}

I've been playing around with the ($b == $lastEl) line, with no luck

===============

Questions I've already looked at, and are not the same OR that created an out of memory error!:

I've tried some of these algorithms with an array of 12 items, and end up running out of memory. However the algorithm that I'm currently using doesn't give me an out of memory error.... BUT.. I need those duplicates!

Community
  • 1
  • 1
Katie
  • 37,767
  • 18
  • 80
  • 110

6 Answers6

11

If you don't mind using a couple of global variables, you could do this in PHP (translated from a version in JavaScript):

<?PHP
$result = array(); 
$combination = array();

function combinations(array $myArray, $choose) {
  global $result, $combination;

  $n = count($myArray);

  function inner ($start, $choose_, $arr, $n) {
    global $result, $combination;

    if ($choose_ == 0) array_push($result,$combination);
    else for ($i = $start; $i <= $n - $choose_; ++$i) {
           array_push($combination, $arr[$i]);
           inner($i + 1, $choose_ - 1, $arr, $n);
           array_pop($combination);
         }
  }
  inner(0, $choose, $myArray, $n);
  return $result;
}

print_r(combinations(array(20,20,22,24), 3));
?>

OUTPUT:

Array ( [0] => Array ( [0] => 20 
                       [1] => 20 
                       [2] => 22 ) 
        [1] => Array ( [0] => 20 
                       [1] => 20 
                       [2] => 24 ) 
        [2] => Array ( [0] => 20 
                       [1] => 22 
                       [2] => 24 ) 
        [3] => Array ( [0] => 20 
                       [1] => 22 
                       [2] => 24 ) ) 
Community
  • 1
  • 1
גלעד ברקן
  • 21,095
  • 3
  • 19
  • 57
3

The pear package Math_Combinatorics makes this kind of problem fairly easy. It takes relatively little code, it's simple and straightforward, and it's pretty easy to read.

$ cat code/php/test.php
<?php
$input = array(20, 20, 22, 24);

require_once 'Math/Combinatorics.php';

$c = new Math_Combinatorics;
$combinations = $c->combinations($input, 3);
for ($i = 0; $i < count($combinations); $i++) {
  $vals = array_values($combinations[$i]);
  $s = implode($vals, ", ");
  print $s . "\n";
}
?>

$ php code/php/test.php
20, 20, 22
20, 20, 24
20, 22, 24
20, 22, 24

If I had to package this as a function, I'd do something like this.

function combinations($arr, $num_at_a_time) 
{
    include_once 'Math/Combinatorics.php';

    if (count($arr) < $num_at_a_time) {
        $arr_count = count($arr);
        trigger_error(
            "Cannot take $arr_count elements $num_at_a_time " 
            ."at a time.", E_USER_ERROR
        );
    }

    $c = new Math_Combinatorics;
    $combinations = $c->combinations($arr, $num_at_a_time);

    $return = array();
    for ($i = 0; $i < count($combinations); $i++) {
        $values = array_values($combinations[$i]);
        $return[$i] = $values;
    }
    return $return;
}

That will return an array of arrays. To get the text . . .

<?php
  include_once('combinations.php');

  $input = array(20, 20, 22, 24);
  $output = combinations($input, 3);

  foreach ($output as $row) {
      print implode($row, ", ").PHP_EOL;
  }
?>
20, 20, 22
20, 20, 24
20, 22, 24
20, 22, 24
Mike Sherrill 'Cat Recall'
  • 82,047
  • 16
  • 110
  • 161
  • why re-invent the wheel ? this is probably the best answer here, I have downloaded the pear class and included it in my project ... thanks – WonderLand Apr 16 '14 at 20:34
2

Why not just use binary? At least then its simple and very easy to understand what each line of code does like this? Here's a function i wrote for myself in a project which i think is pretty neat!

function search_get_combos($array){
$bits = count($array); //bits of binary number equal to number of words in query;
//Convert decimal number to binary with set number of bits, and split into array
$dec = 1;
$binary = str_split(str_pad(decbin($dec), $bits, '0', STR_PAD_LEFT));
while($dec < pow(2, $bits)) {
    //Each 'word' is linked to a bit of the binary number.
    //Whenever the bit is '1' its added to the current term.
    $curterm = "";
    $i = 0;
    while($i < ($bits)){
        if($binary[$i] == 1) {
            $curterm[] = $array[$i]." ";
        }
        $i++;
    }
    $terms[] = $curterm;
    //Count up by 1
    $dec++;
    $binary = str_split(str_pad(decbin($dec), $bits, '0', STR_PAD_LEFT));
}
return $terms;
} 

For your example, this outputs:

Array
(
    [0] => Array
        (
            [0] => 24 
        )
    [1] => Array
        (
            [0] => 22 
        )
    [2] => Array
        (
            [0] => 22 
            [1] => 24 
        )
    [3] => Array
        (
            [0] => 20 
        )
    [4] => Array
        (
            [0] => 20 
            [1] => 24 
        )
    [5] => Array
        (
            [0] => 20 
            [1] => 22 
        )
    [6] => Array
        (
            [0] => 20 
            [1] => 22 
            [2] => 24 
        )
    [7] => Array
        (
            [0] => 20 
        )
    [8] => Array
        (
            [0] => 20 
            [1] => 24 
        )
    [9] => Array
        (
            [0] => 20 
            [1] => 22 
        )
    [10] => Array
        (
            [0] => 20 
            [1] => 22 
            [2] => 24 
        )
    [11] => Array
        (
            [0] => 20 
            [1] => 20 
        )
    [12] => Array
        (
            [0] => 20 
            [1] => 20 
            [2] => 24 
        )
    [13] => Array
        (
            [0] => 20 
            [1] => 20 
            [2] => 22 
        )
    [14] => Array
        (
            [0] => 20 
            [1] => 20 
            [2] => 22 
            [3] => 24 
        )
)
Adi Bradfield
  • 1,893
  • 1
  • 15
  • 26
1

The Idea is simple. Suppose you know how to permute, then if you save these permutations in a set it becomes a combinations. Set by definition takes care of the duplicate values. The Php euqivalent of Set or HashSet is SplObjectStorage and ArrayList is Array. It should not be hard to rewrite. I have an implementation in Java:

public static HashSet<ArrayList<Integer>> permuteWithoutDuplicate(ArrayList<Integer> input){
          if(input.size()==1){
              HashSet<ArrayList<Integer>> b=new HashSet<ArrayList<Integer>>();
              b.add(input);
              return b;
          }
          HashSet<ArrayList<Integer>>ret= new HashSet<ArrayList<Integer>>();
          int len=input.size();
          for(int i=0;i<len;i++){
              Integer a = input.remove(i);
              HashSet<ArrayList<Integer>>temp=permuteWithoutDuplicate(new ArrayList<Integer>(input));
              for(ArrayList<Integer> t:temp)
                  t.add(a);
              ret.addAll(temp);
              input.add(i, a);
          }
          return ret;
      }
faisal
  • 1,259
  • 10
  • 17
1

Had the same problem and found a different and bitwise, faster solution:

function bitprint($u) {
    $s = array();
    for ($n=0; $u; $n++, $u >>= 1){
        if ($u&1){
            $s [] = $n;
        }
    }
    return $s;
}
function bitcount($u) {
    for ($n=0; $u; $n++, $u = $u&($u-1));
    return $n;
}
function comb($c,$n) {
    $s = array();
    for ($u=0; $u<1<<$n; $u++){
        if (bitcount($u) == $c){
            $s [] = bitprint($u);
        }
    }
    return $s;
}

This one generates all size m combinations of the integers from 0 to n-1, so for example m = 2, n = 3 and calling comb(2, 3) will produce:

0 1
0 2
1 2

It gives you index positions, so it's easy to point to array elements by index.

Edit: Fails with input comb(30, 5). Have no idea why, anyone any idea?

  • You're right, this is a translation from javascript and even the javascript code fails with the same input. –  Sep 03 '13 at 15:58
  • It works but comb(30,5) doesn't. I think it's because it's wrong input and 30 it's too high. – Gigamegs Sep 05 '13 at 02:04
1

Cleaned up Adi Bradfield's sugestion using strrev and for/foreach loops, and only get unique results.

function search_get_combos($array = array()) {
sort($array);
$terms = array();

for ($dec = 1; $dec < pow(2, count($array)); $dec++) {
    $curterm = array();
    foreach (str_split(strrev(decbin($dec))) as $i => $bit) {
        if ($bit) {
            $curterm[] = $array[$i];
        }
    }
    if (!in_array($curterm, $terms)) {
        $terms[] = $curterm;
    }
}

return $terms;
}
AJenbo
  • 212
  • 1
  • 3