PHP array combinations

Viewed 36759

I have an array of 7 numbers (1,2,3,4,5,6,7) and I want to choose 5 of the numbers like
(1,2,3,4,5), (1,2,3,4,6), (1,2,3,4,7).
Note that (1,2,3,4,5) is equal to (4,5,3,1,2), so only one of those should be included in the output.

I would like to know if there is a function in PHP or any algorithm that can do this ? I have no idea where to start from. Can you help me ?

I want all the combinations of 7 given numbers ( they are taken from an array ) put into 5 slots, disregarding order.

7 Answers

Another solution that bases on stack. It's quit fast but eats much memory.

Hope that helps someone.

In detail:

function _combine($numbers, $length)
{
    $combinations = array();
    $stack = array();

    // every combinations can be ordered
    sort($numbers);

    // startup
    array_push($stack, array(
        'store' => array(),
        'options' => $numbers,
    ));

    while (true) {
        // pop a item
        $item = array_pop($stack);

        // end of stack
        if (!$item) {
            break;
        }

        // valid store
        if ($length <= count($item['store'])) {
            $combinations[] = $item['store'];
            continue;
        }

        // bypass when options are not enough
        if (count($item['store']) + count($item['options']) < $length) {
            continue;
        }

        foreach ($item['options'] as $index => $n) {
            $newStore = $item['store'];
            $newStore[] = $n;

            // every combine can be ordered
            // so accept only options which is greater than store numbers
            $newOptions = array_slice($item['options'], $index + 1);

            // push new items
            array_push($stack, array(
                'store' => $newStore,
                'options' => $newOptions,
            ));
        }
    }

    return $combinations;
}

Improved this answer to work with associative array as well:

function uniqueCombination($values, $minLength = 1, $maxLength = 2000) {
    $count = count($values);
    $size = pow(2, $count);
    $keys = array_keys($values);
    $return = [];

    for($i = 0; $i < $size; $i ++) {
        $b = sprintf("%0" . $count . "b", $i);
        $out = [];

        for($j = 0; $j < $count; $j ++) {
            if ($b[$j] == '1') {
                $out[$keys[$j]] = $values[$keys[$j]];
            }
        }

        if (count($out) >= $minLength && count($out) <= $maxLength) {
             $return[] = $out;
        }
    }

    return $return;
}

Eg:

print_r(uniqueCombination([
    'a' => 'xyz',
    'b' => 'pqr',
]);

Result:

Array
(
    [0] => Array
        (
            [b] => pqr
        )

    [1] => Array
        (
            [a] => xyz
        )

    [2] => Array
        (
            [a] => xyz
            [b] => pqr
        )

)

It will still work for non-associative arrays:

print_r(uniqueCombination(['a', 'b']);

Result:

Array
(
    [0] => Array
        (
            [1] => b
        )

    [1] => Array
        (
            [0] => a
        )

    [2] => Array
        (
            [0] => a
            [1] => b
        )

)

New solution which optimizes speed and memory for combining algorithm

Mindset: generate combinations K numbers of Array of numbers. New solution will use K 'for' statements. One 'for' One number. Such as: $K = 5 mean that 5 of 'for' statements is used

$total = count($array);
$i0 = -1;
for ($i1 = $i0 + 1; $i1 < $total; $i1++) {
    for ($i2 = $i1 + 1; $i2 < $total; $i2++) {
        for ($i3 = $i2 + 1; $i3 < $total; $i3++) {
            for ($i4 = $i3 + 1; $i4 < $total; $i4++) {
                for ($i5 = $i4 + 1; $i5 < $total; $i5++) {
                    $record = array();
                    for ($i = 1; $i <= $k; $i++) {
                        $t = "i$i";
                        $record[] = $array[$$t];
                    }
                    $callback($record);
                }
            }
        }
    }
}

And detail of code that generated the real code that will be execute by eval() function

function combine($array, $k, $callback)
{
    $total = count($array);
    $init = '
        $i0 = -1;
    ';
    $sample = '
        for($i{current} = $i{previous} + 1; $i{current} < $total; $i{current}++ ) {
            {body}
        }
    ';

    $do = '
        $record = array();
        for ($i = 1; $i <= $k; $i++) {
            $t = "i$i";
            $record[] = $array[$$t];
        }
        $callback($record);
    ';
    $for = '';
    for ($i = $k; $i >= 1; $i--) {
        switch ($i) {
            case $k:
                $for = str_replace(['{current}', '{previous}', '{body}'], [$i, $i - 1, $do], $sample);
                break;
            case 1:
                $for = $init . str_replace(['{current}', '{previous}', '{body}'], [$i, $i - 1, $for], $sample);
                break;
            default:
                $for = str_replace(['{current}', '{previous}', '{body}'], [$i, $i - 1, $for], $sample);
                break;
        }
    }

    // execute
    eval($for);
}

How to combine K numbers of Array

$k = 5;
$array = array(1, 2, 3, 4, 5, 6, 7);
$callback = function ($record) {
    echo implode($record) . "\n";
};
combine($array, $k, $callback);

I needed a combining function that included subsets, so I took @Nguyen Van Vinh's answer and modified it for my needs.

If you pass [1,2,3,4] to the function, it returns every unique combination and subset, sorted:

[
  [1,2,3,4], [1,2,3], [1,2,4], [1,3,4], [2,3,4], [1,2], [1,3], [1,4], [2,3], [2,4], [3,4], [1], [2], [3], [4]
]

Here's the function:

function get_combinations_with_length( $numbers, $length ){
    $result = array();
    $stack = array();
    // every combinations can be ordered
    sort($numbers);
    // startup
    array_push($stack, array(
        'store' => array(),
        'options' => $numbers,
    ));
    while (true) {
        // pop a item
        $item = array_pop($stack);
        // end of stack
        if (!$item) break;
        // valid store
        if ($length <= count($item['store'])) {
            $result[] = $item['store'];
            continue;
        }
        // bypass when options are not enough
        if (count($item['store']) + count($item['options']) < $length) {
            continue;
        }
        foreach ($item['options'] as $i=>$n) {
            $newStore = $item['store'];
            $newStore[] = $n;
            // every combine can be ordered, so accept only options that are greater than store numbers
            $newOptions = array_slice($item['options'], $i + 1);
            // array_unshift to sort numerically, array_push to reverse
            array_unshift($stack, array(
                'store' => $newStore,
                'options' => $newOptions,
            ));
        }
    }
    return $result;
}

function get_all_combinations( $numbers ){
    $length = count($numbers);
    $result = [];
    while ($length > 0) {
        $result = array_merge($result, get_combinations_with_length( $numbers, $length ));
        $length--;
    }
    return $result;
}


$numbers = [1,2,3,4];
$result = get_all_combinations($numbers);

echo 'START: '.json_encode( $numbers ).'<br><br>';
echo 'RESULT: '.json_encode( $result ).'<br><br>';
echo '('.count($result).' combination subsets found)';
Related