PHP Array: объединить все подматрицы вместе (вероятность)

Я хочу просто найти лучший способ сделать это:

$array = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l')
);

Цель состоит в том, чтобы напечатать что-то вроде этого:

a e h
a e i
a e j
a e k
a e l

a f h
a f i
a f j
a f k
a f l

a g h
a g i
a g j
a g k
a g l

Затем сделаем то же самое для b и c.

В настоящее время я использую этот код:

foreach ($array[0] as $val1) {
    foreach ($array[1] as $val2) {
        foreach ($array[2] as $val3) {
            echo "$val1  $val2  $val3 \n";
        }
        echo "--------\n";
    }
}

Я также попытался создать выше код динамически и выполнить его с помощью eval:

$eval         = '
     $data =array();
     ';
$eval_blocks  = '';
$eval_foreach = '';
$eval_data    = '
    $data[] = ';
$looplength   = count($array);

for ($i = 0; $i < $looplength; $i++) {
    $eval_foreach .= '
     foreach($array[' . $i . '] as $val' . ($i + 1) . '){
     ';
    if (($i + 1) == $looplength) {
        $eval_data .= ' $val' . ($i + 1) . ';';
    } else {
        $eval_data .= ' $val' . ($i + 1) . ' ." ".';
    }
    $eval_blocks .= '
     }
     ';
}
$eval = $eval . $eval_foreach . $eval_data . $eval_blocks;
eval($eval);
print_r($data);

Но я все еще хочу найти лучший способ сделать это, если это возможно.

Update:

Примечание: $array является динамическим, он может содержать два под-массива или более

Ответ 1

это может помочь вам попробовать это.

    class Test {
    // holds the combinations


     var $combinations= array(); 



    function getCombinations($batch, $elements, $i)  {
        if ($i >= count($elements)) {
            $this->combinations[] = $batch;
        } else {     
            foreach ($elements[$i] as $element) {
                $this->getCombinations(array_merge($batch, $element), $elements, $i + 1);
            }                       
        }
    }
}  

//Проверьте это!

$traits = array (
    array(
        array('Happy'),
        array('Sad'),
        array('Angry'),
        array('Hopeful')
    ),
    array(
        array('Outgoing'),
        array('Introverted')
    ),
    array(
        array('Tall'),
        array('Short'),
        array('Medium')
    ),
    array(
        array('Violent'),
        array('Quiet'),
        array('Psychotic')
    ),
    array(
        array('Handsome'),
        array('Plain'),
        array('Ugly')
    )
);

$test = new Test();
$start = array();
$test->getCombinations($start, $traits, 0);   
print_r($test->combinations);   

Ответ 2

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

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

Вот код:

function getCombinations( $arrayList, $index = 0  )
{
    $subCombinations = $combinations = '';

    if ( $index < count( $arrayList )-1 )
    {
        $subCombinations = getCombinations( $arrayList,  $index+1 );
    }

    foreach( $arrayList[$index] as $item )
    {
        $combinations[$item] = $subCombinations ;
    }
    return $combinations;
}

$combinations = getCombinations( $array );

print_r( $combinations );

С примерами данных:

$array = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l')
);

Он выведет:

Array
(
    [a] => Array
        (
            [e] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

            [f] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

            [g] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

        )

    [b] => Array
        (
            [e] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

            [f] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

            [g] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

        )

    [c] => Array
        (
            [e] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

            [f] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

            [g] => Array
                (
                    [h] => 
                    [i] => 
                    [j] => 
                    [k] => 
                    [l] => 
                )

        )

)

И затем для получения ожидаемого результата требуется дополнительный код:

function drawCombinations( $combinations, $line = array() )
{
    foreach( $combinations as $value => $children )
    {
        array_push( $line, $value );

        if ( is_array( $children ) ) 
        {
            drawCombinations( $children, $line );
        }
        else
        { 
            echo implode( " ", $line ) ." \n";
        }

        array_pop( $line );
    }

}


drawCombinations( $combinations );

Чтобы произвести:

a e h
a e i
a e j
a e k
a e l
a f h
a f i
a f j
a f k
a f l
a g h
a g i
a g j
a g k
a g l
b e h
b e i
b e j
b e k
b e l
b f h
b f i
b f j
b f k
b f l
b g h
b g i
b g j
b g k
b g l
c e h
c e i
c e j
c e k
c e l
c f h
c f i
c f j
c f k
c f l
c g h
c g i
c g j
c g k
c g l

Как я уже говорил, если у вас нет этого дерева результатов (о том, что ваши вопросы не упоминались, я просто произвел это при поиске наилучшего пути), подход Valentin CLEMENT может быть лучше (если вы не используете слишком большой набор данных, я объясню, почему после).

Я переписал его немного, в каком-то смысле, я думаю, более читабельным и удобным:

function expand( $array, $from = 0, $length = false ) 
{
    if ( $length === false )
    {
        $length = count( $array );
    }

    if ( $length == $from )
    {
        return array('');
    }
    else 
    {
        $result = array();

        foreach( $array[$from] as $x ) 
        {
            foreach( expand( $array, $from+1, $length ) as $tail )
            {
                $result[] = trim("$x $tail");
            }
        }
        return $result;
    }
}


$combinations = expand( $array );

print_r( $combinations );

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

Array
(
    [0] => a e h
    [1] => a e i
    [2] => a e j
    [3] => a e k
    [4] => a e l
    [5] => a f h
    [6] => a f i
    [7] => a f j
    [8] => a f k
    [9] => a f l
    [10] => a g h
    [11] => a g i
    [12] => a g j
    [13] => a g k
    [14] => a g l
    [15] => b e h
    [16] => b e i
    [17] => b e j
    [18] => b e k
    [19] => b e l
    [20] => b f h
    [21] => b f i
    [22] => b f j
    [23] => b f k
    [24] => b f l
    [25] => b g h
    [26] => b g i
    [27] => b g j
    [28] => b g k
    [29] => b g l
    [30] => c e h
    [31] => c e i
    [32] => c e j
    [33] => c e k
    [34] => c e l
    [35] => c f h
    [36] => c f i
    [37] => c f j
    [38] => c f k
    [39] => c f l
    [40] => c g h
    [41] => c g i
    [42] => c g j
    [43] => c g k
    [44] => c g l
)

И тогда легко достичь ожидаемого результата:

echo implode( "\n", $combinations )."\n";

Будет выводиться:

a e h
a e i
a e j
a e k
a e l
a f h
a f i
a f j
a f k
a f l
a g h
a g i
a g j
a g k
a g l
b e h
b e i
b e j
b e k
b e l
b f h
b f i
b f j
b f k
b f l
b g h
b g i
b g j
b g k
b g l
c e h
c e i
c e j
c e k
c e l
c f h
c f i
c f j
c f k
c f l
c g h
c g i
c g j
c g k
c g l

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

Отображение метрик памяти по двум методам дало такие результаты:

drawCombinations(  getCombinations( $array ));

echo memory_get_usage()." ". memory_get_peak_usage()."\n";

// 238736 244896





echo implode( "\n", expand( $array ) )."\n";

echo memory_get_usage()." ". memory_get_peak_usage()."\n";

// 238912 252304

Но это становится более важным при использовании больших входных значений:

$array = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l'),
    array('m', 'n', 'o', 'p', 'q', 'r', 's'),
    array('t', 'u', 'v', 'x', 'y', 'z')
);

getCombinations дает:

drawCombinations(  getCombinations( $array ));

echo memory_get_usage()." ". memory_get_peak_usage()."\n";

// 242376 253008

expand дает:

echo implode( "\n", expand( $array ) )."\n";

echo memory_get_usage()." ". memory_get_peak_usage()."\n";

//242544 704520

Причина очевидна, если мы посмотрим на массив, создаваемый каждой функцией, поскольку первое решение хранит меньше повторяющихся значений (я не уверен, как PHP обрабатывает дубликаты массивов, заканчивая каждую ветвь дерева).

Еще раз, в зависимости от того, что вы просто достигнете, вы позаботитесь или не будете.

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

Я надеюсь, что это поможет, по крайней мере, это было интересно мне;)

Ответ 3

Сначала я выбрал какое-то решение на основе итерации (подсчета), которое непосредственно работает с массивами. Как оказалось, это был всего лишь цикл с несколькими счетчиками. Тогда я подумал: зачем ограничивать это на массивы? Почему бы не использовать CartesianProductIterator, который создает итерацию над декартовым произведением нескольких итераторов?

Он работает аналогично AppendIterator и MultipleIterator (см. также: Итерация по нескольким итераторам один раз (апрель 2012 г. по hakre)) и может быть легко использована с вашим массивом:

$array = [
    ['a', 'b', 'c'],
    ['e', 'f', 'g'],
    ['h', 'i', 'j', 'k', 'l'],
];

$it = new CartesianProductIterator();

foreach($array as $sub)
{
    $it->append(new ArrayIterator($sub));
}

foreach ($it as $tuple)
{
    echo implode(',', $tuple), "\n";
}

Вывод будет таким, как ожидалось:

a,e,h
a,e,i
a,e,j
...
c,g,j
c,g,k
c,g,l

Преимущество такого итератора состоит в том, что он более гибкий для того, что он принимает в качестве входных данных.

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

Другим преимуществом является то, что Iterator уже отслеживает подсчет, поэтому проблема подсчета для каждого измерения продукта уже решена.

Пока используется потребление памяти, вместо итератора, который выполняет подсчет для вас, вы можете выполнить итерацию по всем кортежам в продукте, а также с помощью цикла. Этот вариант кстати. должен действительно заботиться о ключах, поэтому должен работать с различными входами (пример кода для чтения):

...

// working loop
$valid = TRUE;
while ($valid)
{
    // create segment
    foreach ($array as $key => $sub)
    {
        echo $sub[$keys[$key][$counters[$key]]];
    }
    echo "\n";

    // count up
    foreach ($order as $key)
    {
        if (++$counters[$key] == $lengths[$key])
        {
            $counters[$key] = 0;
            continue;
        }
        continue 2;
    }
    $valid = FALSE;
};

Как показывает этот пример, каждая итерация в цикле выводит один кортеж продукта, поэтому требования к памяти также низки. Если вы замените echo на инструкцию yield, это хороший шаблон для создания generator.

Поскольку объект CartesianProdcutIterator является объектом, поэтому он может делать немного больше, чем цикл или генератор, поэтому он имеет еще несколько функций: вы можете указать режим итерации или подсчета или сортировки: сначала перенести на последний итератор (по умолчанию), или первый первый:

$it = new CartesianProductIterator(CartesianProductIterator::ORDER_FIRST_FIRST);

Это сделает следующую итерацию:

a,e,h
b,e,h
c,e,h
...
a,g,l
b,g,l
c,g,l

Но не только это, его можно даже контролировать больше, указав параметр $countOrder при добавлении. Он определяет фактические ключи сортировки, которые должны быть упорядочены в режиме заказа:

$array = [
    0 => ['a', 'b', 'c'],
    2 => ['e', 'f', 'g'],
    1 => ['h', 'i', 'j', 'k', 'l'],
];

$it = new CartesianProductIterator();

foreach($array as $countOrder => $sub)
{
    $it->append(new ArrayIterator($sub), $countOrder);
}

foreach ($it as $tuple)
{
    echo implode(',', $tuple), "\n";
}

Это (поскольку режим по умолчанию последний - первый) указывает на первую итерацию в середине (e-g), затем в конце (h-l), а затем на первом (a-c):

a,e,h
a,f,h
a,g,h
a,e,i
...
c,g,k
c,e,l
c,f,l
c,g,l

Надеюсь, что это полезно и квалифицируется как "лучший способ".

Ответ 4

Это должно работать:

function expand($arr){
    function recexpand($arr, $from, $len) {
        if ($from == $len) {
            yield "\n";
        } else {
            foreach($arr[$from] as $x) {
                foreach(expand($arr, $from+1, $len) as $tail) {
                    yield "$x $tail";
                }
            }
        }
    }
    return recexpand($arr, 0, count($arr);
}
$array = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l')
);
foreach(expand($array) as $row) {
    echo $row;
}

отголоски:

a e h
a e i
a e j
a e k
a e l
a f h
a f i
a f j
a f k
a f l
a g h
a g i
a g j
a g k
a g l
b e h
b e i
b e j
b e k
b e l
b f h
b f i
b f j
b f k
b f l
b g h
b g i
b g j
b g k
b g l
c e h
c e i
c e j
c e k
c e l
c f h
c f i
c f j
c f k
c f l
c g h
c g i
c g j
c g k
c g l

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

Для PHP < 5 (или любой версии, которая не имеет оператора 'yield')

function expand($arr) {
    function recexpand($arr, $from, $len) {
        if ($from == $len) {
            return array("\n");
        } else {
            $result = array();
            foreach($arr[$from] as $x) {
                foreach(expand($arr, $from+1, $len) as $tail) {
                    $result[] = "$x " . $tail;
                }
            }
            return $result;
        }
    }
    return recexpand($arr, 0, count($arr));
}
$arr = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l')
);
foreach(expand($arr) as $row) {
    echo "$row";
}

Ответ 5

Я впечатлен великолепными алгоритмами, которые вы, ребята, придумали. Однако я считаю, что правильным методом в PHP является использование Iterator: http://php.net/manual/fr/class.iterator.php

Я применил пример того, что вы могли бы сделать для своей проблемы.

class CombinationIterator implements Iterator {
    private $position = 0;
    private $array = array();

    public function __construct($array) {
        $this->array = $array;
        $this->position = array_fill(0,sizeof($this->array),0);
    }

    function rewind() {
        $this->position = array_fill(0,sizeof($this->array),0);
    }

    function current() {
        $word = array();
        foreach ($this->position as $i=>$pos) {
            $word[]=$this->array[$i][$pos];
        }
        return implode(" ",$word);
    }

    function key() {
        return $this->position;
    }
    function next() {
        foreach (array_reverse($this->position,true) as $i=>$pos) {
            # if position in this array has reached end, set it to 0 and increse next one
            if ($pos == sizeof($this->array[$i])-1) {
                $this->position[$i] = 0;
                if (array_key_exists($i-1,$this->position)) {
                    continue;
                } else {
                    $this->rewind();
                }
                break;
            } else {
                $this->position[$i]++;
                break;
            }

        }
    }
    function valid() {
        $valid = false;
        foreach ($this->position as $i=>$pos) {
            if ($pos < sizeof($this->array[$i])-1) { return true; }
        }
        return $valid;
    }

}

И вот как вы могли использовать его для отображения своих слов:

$array = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l'),
    array('m', 'n', 'o', 'p', 'q', 'r', 's'),
    array('t', 'u', 'v', 'x', 'y', 'z')
);


$c = new CombinationIterator($array);
while ($c->valid()) {
    echo $c->current()."\n";
    $c->next();
}

Я не писал метод previous(), но вы можете легко создать его из метода next().

Также здесь очень мало использования памяти, потому что вы сохраняете только позицию.

Надеюсь, это поможет вам в вашем проекте.

Bye

Ответ 6

Мой нерекурсивный способ:

<?php
$array = array(
    array('a', 'b', 'c'),
    array('e', 'f', 'g'),
    array('h', 'i', 'j', 'k', 'l')
);

$upperBounds = array();
foreach ($array as $arr) {
    $upperBounds[] = count($arr);
}

$counterArray = array_pad(array(), count($array), 0);

do {
    foreach ($counterArray as $key => $c) {
        echo $array[$key][$c];
    }
    echo PHP_EOL;
} while (incrementCounter($counterArray, $upperBounds));

function incrementCounter(&$counterArray, $upperBounds)
{
    if (count($counterArray) == 0 || count($counterArray) != count($upperBounds)) {
        return false;
    }
    $counterArray[0]++;
    foreach ($counterArray as $key => $value) {
        if ($counterArray[$key] >= $upperBounds[$key]) {
            $counterArray[$key] = 0;
            if (isset($counterArray[$key+1])) {
                $counterArray[$key+1]++;
            }
            else {
                $counterArray[$key+1] = 1;
                return false;
            }
        }
        else {
            break;
        }
    }
    return true;
}