Комбинаторика массивов в PHP

рассмотрим следующий массив:

$a = [['x'], ['y', 'z', 'w'], ['m', 'n']];

Как можно сгенерировать из него следующий массив:

$output=[
[[x][y][m]],
[[x][z][n]],
[[x][w][m]],
[[x][y][n]],
[[x][z][m]],
[[x][w][n]],
];

Я ищу более эффективный код, чем у меня. (Мой текущий код представлен как ответ ниже)

5 ответов


поехали. Предполагая:

$array = [['x'], ['y', 'z', 'w'], ['m', 'n']];

EDIT: после некоторого тестирования производительности я пришел к выводу, что решение, которое я опубликовал раньше, примерно на 300% медленнее кода OP, конечно, из-за штабелирования вложенных вызовов функций. Итак, вот улучшенная версия подхода OP, которая находится вокруг 40% быстрее:

$count     = array_map('count', $array);
$finalSize = array_product($count);
$arraySize = count($array);
$output    = array_fill(0, $finalSize, []);
$i = 0;
$c = 0;
for (; $i < $finalSize; $i++) {
    for ($c = 0; $c < $arraySize; $c++) {
        $output[$i][] = $array[$c][$i % $count[$c]];
    }
}

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


"более эффективный код" - такая субъективная вещь .... ;-)
Вы можете использовать итераторы вместо массивов, поэтому полный результат не нужно хранить в памяти. С другой стороны, это решение, скорее всего, намного медленнее.

<?php
class PermIterator implements Iterator {
    protected $mi;
    protected $finalSize, $pos;

    public function __construct(array $src) {
        $mi = new MultipleIterator;
        $finalSize = 1;
        foreach ( $src as $a ) {
            $finalSize *= count($a);
            $mi->attachIterator( new InfiniteIterator(new ArrayIterator($a)) );
        }
        $this->mi = $mi;
        $this->finalSize = $finalSize;
        $this->pos = 0;
    }

    public function current() { return $this->mi->current(); }
    public function key() { return $this->mi->key(); }
    public function next() { $this->pos+=1; $this->mi->next(); }
    public function rewind() { $this->pos = 0; $this->mi->rewind(); }
    public function valid() { return ($this->pos < $this->finalSize) && $this->mi->valid(); }
}


$src = $a = [['x'], ['y', 'z', 'w'], ['m', 'n']];
$pi = new PermIterator($src); // <- you can pass this one around instead of the array
foreach ( $pi as $e ) {
    echo join(', ', $e), "\n";
}

печать

x, y, m
x, z, n
x, w, m
x, y, n
x, z, m
x, w, n

или как массив (объект), где вы можете получить доступ к каждому элементу через целое смещение

<?php
class PermArray implements  ArrayAccess {
    // todo: constraints and error handling - it's just an example
    protected $source;
    protected $size;

    public function __construct($source) {
        $this->source = $source;
        $this->size = 1;
        foreach ( $source as $a ) {
            $this->size *= count($a);
        }
    }
    public function count() { return $this->size; }

    public function offsetExists($offset) { return is_int($offset) && $offset < $this->size; }
    public function offsetGet($offset) {
        $rv = array();
        for ($c = 0; $c < count($this->source); $c++) {
          $index = ($offset + $this->size) % count($this->source[$c]);
          $rv[] = $this->source[$c][$index];
        }
        return $rv;
    }

    public function offsetSet($offset, $value ){}
    public function offsetUnset($offset){}
}

$pa = new PermArray( [['x'], ['y', 'z', 'w'], ['m', 'n']] );
$cnt = $pa->count();
for($i=0; $i<$cnt; $i++) {
    echo join(', ', $pa[$i]), "\n";
}

<?php
function array_permutation(array $a)
{
    $count = array_map('count', $a);
    $finalSize = 1;

    foreach ($count as $val) {
        $finalSize *= $val;
    }

    $output = [];

    for ($i = 0; $i < $finalSize; $i++) {
        $output[$i] = [];
        for ($c = 0; $c < count($a); $c++) {
            $index = ($i + $finalSize) % $count[$c];
            array_push($output[$i], $a[$c][$index]);
        }
    }
    return $output;
}

$a = [['x'], ['y', 'z', 'w'], ['m', 'n']];
$output= array_permutation($a);

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

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

// the input column sets to be permuted
$column_sets = [
    [ //set 1
        ['Column 1' => 'c1v1']
    ],
    [ //set 2
        ['column 2' => 'c2v1', 'Column 3' => 'c3v1'],
        ['column 2' => 'c2v2', 'Column 3' => 'c3v2'],
    ],
    [ //set 3
        ['Column 4' => 'c4v1', 'Column 5' => 'c5v1'],
        ['Column 4' => 'c4v2', 'Column 5' => 'c5v2'],
    ],
    [ //set 4
        ['Column 6' => 'c6v1', 'Column 7' => 'c7v1'],
        ['Column 6' => 'c6v2', 'Column 7' => 'c7v2'],
        ['Column 6' => 'c6v3', 'Column 7' => 'c7v3'],
    ],
    [ //set 5
        ['Column 8' => 'c8v1', 'Column 9' => 'c9v1'],
        ['Column 8' => 'c8v2', 'Column 9' => 'c9v2'],
        ['Column 8' => 'c8v3', 'Column 9' => 'c9v3'],
    ],
];

// copy the first set into the output to start
$output_rows = $column_sets[0];

foreach ($column_sets as $column_set) {
    // save the current state of the rows for use within this loop
    $current_output = $output_rows;

    // calculate the number of permutations necessary to combine the output rows with the current column set
    $permutations = count($output_rows) * count($column_set);
    for ($permutation=0; $permutation < $permutations; $permutation++) {

        // if the current permutation index is grater than the number of rows in the output,
        // then copy a row from the pre-permutation output rows, repeating as necessary
        if ($permutation > count($output_rows) - 1) {
            $output_rows[] = $current_output[$permutation % count($current_output)];
        }

        // copy the columns from the column set
        foreach ($column_set[0] as $key => $value) {
            $output_rows[$permutation][$key] = $column_set[intval($permutation / count($current_output)) % count($column_set)][$key];
        }
    }
}


echo "After Permutaion:\n";
echo implode("\t", array_keys($output_rows[0])) . PHP_EOL;
foreach ($output_rows as $row) {
    echo implode("\t", array_values($row)) . PHP_EOL;
}

[x][y][z]
[x][z][y]
[y][x][z]
[y][z][x]
[z][x][y]
[z][y][x]

Я думаю, что ваша проблема не перестановка, а скорее комбинаторика, потому что если перестановка ваш код выведет то,что написано выше,если задан массив {x, y, z} формула для перестановки-это n!(n-1)! в данном случае-6. Итак, шесть перестановок.