Поиск декартового произведения с помощью ассоциативных массивов PHP - PullRequest
46 голосов
/ 11 июня 2011

Скажите, что у меня есть массив, подобный следующему:

Array
(
    [arm] => Array
        (
            [0] => A
            [1] => B
            [2] => C
        )
    [gender] => Array
        (
            [0] => Female
            [1] => Male
        )
    [location] => Array
        (
            [0] => Vancouver
            [1] => Calgary
        )
)

Как найти декартово произведение, сохранив ключи внешнего ассоциативного массива и используя их во внутренних? Результат работы алгоритма должен быть таким:

Array
(
    [0] => Array
        (
            [arm] => A
            [gender] => Female
            [location] => Vancouver
        )

    [1] => Array
        (
            [arm] => A
            [gender] => Female
            [location] => Calgary
        )

    [2] => Array
        (
            [arm] => A
            [gender] => Male
            [location] => Vancouver
        )

...etc.

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

    $result = array();
    foreach ($map as $a) {
        if (empty($result)) {
            $result = $a;
            continue;
        }
        $res = array();
        foreach ($result as $r) {
            foreach ($a as $v) {
                $res[] = array_merge((array)$r, (array)$v);
            }
        }
        $result = $res;
    }

    print_r($result);

Любая помощь будет оценена.

Ответы [ 10 ]

53 голосов
/ 11 июня 2011

Вот решение, которое мне не стыдно показать.

Обоснование

Предположим, что у нас есть входной массив $input с N подмассивами, как в вашем примере. каждый подмассив имеет Cn элементов, где n - его индекс внутри $input, а его ключ - Kn. Я буду ссылаться на i th-й элемент n th-массива как Vn,i.

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

1) Для N = 1 декартово произведение будет просто array(0 => array(K1 => V1,1), 1 => array(K1 => V1,2), ... ) - всего С1. Это можно сделать с помощью простого foreach.

2) Предположим, что $result уже содержит декартово произведение первых поднаборов N-1. Декартово произведение $result и N-й подматрицы можно получить следующим образом:

3) В каждом элементе (массиве) внутри $product добавьте значение KN => VN,1. Запомните получившийся предмет (с добавленной стоимостью); Я буду называть это $item.

4a) Для каждого массива внутри $product:

4b) Для каждого значения в наборе VN,2 ... VN,CN добавьте к $product копию $item, но измените значение с помощью клавиши KN на VN,m (для всех 2 <= m <= CN).

Две итерации 4a (более $product) и 4b (по N-му входному подмассиву) заканчиваются $result, имеющими CN элементов для каждого элемента, который имел до итераций, поэтому в итоге $result действительно содержит декартово произведение первых N вложенных массивов.

Поэтому алгоритм будет работать для любого N.

Это было сложнее написать, чем следовало бы. Мои официальные доказательства определенно ржавеют ...

Код

function cartesian($input) {
    $result = array();

    while (list($key, $values) = each($input)) {
        // If a sub-array is empty, it doesn't affect the cartesian product
        if (empty($values)) {
            continue;
        }

        // Seeding the product array with the values from the first sub-array
        if (empty($result)) {
            foreach($values as $value) {
                $result[] = array($key => $value);
            }
        }
        else {
            // Second and subsequent input sub-arrays work like this:
            //   1. In each existing array inside $product, add an item with
            //      key == $key and value == first item in input sub-array
            //   2. Then, for each remaining item in current input sub-array,
            //      add a copy of each existing array inside $product with
            //      key == $key and value == first item of input sub-array

            // Store all items to be added to $product here; adding them
            // inside the foreach will result in an infinite loop
            $append = array();

            foreach($result as &$product) {
                // Do step 1 above. array_shift is not the most efficient, but
                // it allows us to iterate over the rest of the items with a
                // simple foreach, making the code short and easy to read.
                $product[$key] = array_shift($values);

                // $product is by reference (that's why the key we added above
                // will appear in the end result), so make a copy of it here
                $copy = $product;

                // Do step 2 above.
                foreach($values as $item) {
                    $copy[$key] = $item;
                    $append[] = $copy;
                }

                // Undo the side effecst of array_shift
                array_unshift($values, $product[$key]);
            }

            // Out of the foreach, we can add to $results now
            $result = array_merge($result, $append);
        }
    }

    return $result;
}

Использование

$input = array(
    'arm' => array('A', 'B', 'C'),
    'gender' => array('Female', 'Male'),
    'location' => array('Vancouver', 'Calgary'),
);

print_r(cartesian($input));
37 голосов
/ 12 апреля 2013

Вот оптимизированная версия декартовой функции @ Джона:

function cartesian($input) {
    $result = array(array());

    foreach ($input as $key => $values) {
        $append = array();

        foreach($result as $product) {
            foreach($values as $item) {
                $product[$key] = $item;
                $append[] = $product;
            }
        }

        $result = $append;
    }

    return $result;
}

Подробнее о математике этого алгоритма: http://en.wikipedia.org/wiki/Cartesian_product

См. Другие примеры этого алгоритма на разных языках:https://rosettacode.org/wiki/Cartesian_product_of_two_or_more_lists

7 голосов
/ 04 июля 2017

В PHP 7 @ ответ Сергея можно сократить до:

function cartesian(array $input)
{
    $result = [[]];
    foreach ($input as $key => $values) {
        $append = [];
        foreach ($values as $value) {
            foreach ($result as $data) {
                $append[] = $data + [$key => $value];
            }
        }
        $result = $append;
    }

    return $result;
}
7 голосов
/ 11 июня 2011

Вот что я мог придумать:

function inject($elem, $array) {
    return array_map(function ($n) use ($elem) { return array_merge((array)$elem, (array)$n); }, $array);
}

function zip($array1, $array2) {
    return array_reduce($array1, function ($v, $n) use ($array2) { return array_merge($v, inject($n, $array2));  }, array());
}

function cartesian_product($array) {
    $keys = array_keys($array);
    $prod = array_shift($array);
    $prod = array_reduce($array, 'zip', $prod);
    return array_map(function ($n) use ($keys) { return array_combine($keys, $n); }, $prod);
}

(Использование псевдомассива / списка / словарной нотации ниже, поскольку PHP просто слишком многословен для таких вещей.)

Функция inject преобразует a, [b] в [(a,b)], то есть вводит одно значение в каждое значение массива, возвращая массив массивов. Не имеет значения, является ли a или b массивом, он всегда будет возвращать двумерный массив.

inject('a', ['foo', 'bar'])
    =>  [('a', 'foo'), ('b', 'bar')]

Функция zip применяет функцию inject к каждому элементу в массиве.

zip(['a', 'b'], ['foo', 'bar'])
    =>  [('a', 'foo'), ('a', 'bar'), ('b', 'foo'), ('b', 'bar')]

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

zip(zip(['a', 'b'], ['foo', 'bar']), ['42', '76'])
    =>  [('a', 'foo', '42'), ('a', 'foo', '76'), ('a', 'bar', '42'), …]

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

array_combine(['key1', 'key2', 'key3'], ['a', 'foo', '42'])
    =>  [ key1 : 'a', key2 : 'foo', key3 : '42' ]

Применение этого ко всем элементам в продукте дает желаемый результат.

Вы можете свести вышеупомянутые три функции в одно длинное выражение, если хотите (что также прояснит ошибки).


«Развернутая» версия без анонимных функций для PHP <= 5.2 будет выглядеть так: </p>

function inject($elem, $array) {
    $elem = (array)$elem;
    foreach ($array as &$a) {
        $a = array_merge($elem, (array)$a);
    }
    return $array;
}

function zip($array1, $array2) {
    $prod = array();
    foreach ($array1 as $a) {
        $prod = array_merge($prod, inject($a, $array2));
    }
    return $prod;
}

function cartesian_product($array) {
    $keys = array_keys($array);
    $prod = array_shift($array);
    $prod = array_reduce($array, 'zip', $prod);

    foreach ($prod as &$a) {
        $a = array_combine($keys, $a);
    }
    return $prod;
}
5 голосов
/ 26 августа 2016

Почему бы не использовать рекурсивный генератор ... проблемы с памятью: почти нет
(и это красиво)

function cartesian($a)
{
    if ($a)
    {
        if($u=array_pop($a))
            foreach(cartesian($a)as$p)
                foreach($u as$v)
                    yield $p+[count($p)=>$v];
    }
    else
        yield[];
}

примечание: это не сохраняет ключи; но это только начало.

Это должно сделать (не проверено):

function acartesian($a)
{
    if ($a)
    {
        $k=end(array_keys($a));
        if($u=array_pop($a))
            foreach(acartesian($a)as$p)
                foreach($u as$v)
                    yield $p+[$k=>$v];
    }
    else
        yield[];
}
3 голосов
/ 13 апреля 2013

Другое решение:

function getAllVariations($input) {
    $result = array();
    $cnt = array_product(array_map('count', $input));
    $step = 1;
    foreach ($input as $key=>$array) {
        for ($i=0; $i<$cnt; $i++) {
            foreach ($array as $value) {
                for ($k=0; $k<$step; $k++) {
                    $result[$i+$k][$key] = $value;
                }
                $i += $step;
            }
            $i--;
        }
        $step = $step * count($array);
    }
    return $result;
}

Использование:

$input = array(
    'arm' => array('A', 'B', 'C'),
    'gender' => array('Female', 'Male'),
    'location' => array('Vancouver', 'Calgary'),
    'name' => array('Rio', 'Mark')
);

echo "<pre>";
var_dump(getAllVariations($input));
2 голосов
/ 11 июня 2011

Я быстро скорректировал ваш код, моя попытка грубая, я думаю, но посмотрим, работает ли это так, как вы хотите:

$result = array();
$nm = '';
foreach ($map as $name => $a) {
    if (empty($result)) {
        $result = $a;
        $nm = $name;
        continue;
    }

    $res = array();
    foreach ($result as $r) {
        foreach ($a as $v) {
            $myr = $r;
            $myv = $v;
            if(!is_array($r)) $myr = array($nm => $r);
            if(!is_array($v)) $myv = array($name => $v);

            $res[] = array_merge($myr, $myv);
        }
    }
    $result = $res;
}
echo "<pre>";
print_r($result);
1 голос
/ 30 апреля 2017

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

function cartezianIterator($inputArray)
{
    $maximumPosition = array_map('count', $inputArray);
    $position = array_pad([], count($inputArray), 0);

    while (false !== ($item = buildItemAtPosition($inputArray, $position))) {

        yield $item;

        $position = incrementPosition($position, $maximumPosition);
    }
}

function buildItemAtPosition($inputArray, $positions)
{
    if ($positions[0] >= count($inputArray[0])) {
        return false;
    }

    $item = [];
    foreach ($inputArray as $rowIndex => $row) {
        $position = $positions[$rowIndex];

        $item[] = $row[$position];
    }

    return $item;
}

function incrementPosition($position, $maximumPosition)
{
    $digitToIncrement = count($position) - 1;

    do {
        $position[$digitToIncrement]++;

        if ($position[$digitToIncrement] < $maximumPosition[$digitToIncrement] || 0 === $digitToIncrement) {
            //no overflow
            break;
        }

        //overflow, reset to zero and increment parent digit
        $position[$digitToIncrement] = 0;

        $digitToIncrement--;
    } while ($digitToIncrement >= 0);

    return $position;
}

Затем, чтобы получить одно решение за раз, вы можете использовать foreach или next, например:

$iterator = cartezianIterator($inputArray);

//of course, you need to do something with the result...
$combination = next($iterator);
$combination = next($iterator);
$combination = next($iterator);
$combination = next($iterator);
$combination = next($iterator);
$combination = next($iterator);

Это решение очень очень быстрое, если вам нужно всего несколько комбинаций.Кроме того, потребление памяти очень низкое (для хранения некоторого integers используется плоский array).

Примечание: рекурсивные функции не используются.

1 голос
/ 11 июня 2011

Почему бы не использовать базу данных для этого?

Это просто в MySQL ..

table arm
   id integer primary key
   label char

table gender
   id integer primary key
   gender enum('male','female')

table location
   id integer primary key
   city varchar(255)

Затем выполните запрос

$query = mysql_query(" 
  SELECT a.label, g.gender, l.city
  FROM arm a
  CROSS JOIN gender g
  CROSS JOIN location l
  ORDER BY a.id
") or die("Could not execute query");

while($row = mysql_fetch_array($query) )
{
   ....
}

И прочитайте это:

0 голосов
/ 30 апреля 2017

Один алгоритм заключается в расширении на каждом шаге предыдущих результатов элементами текущего шага:

function cartezian1($inputArray)
{
    $results = [];

    foreach ($inputArray as $group) {
        $results = expandItems($results, $group);
    }

    return $results;
}

function expandItems($sourceItems, $tails)
{
    $result = [];

    if (empty($sourceItems)) {
        foreach ($tails as $tail) {
            $result[] = [$tail];
        }
        return $result;
    }

    foreach ($sourceItems as $sourceItem) {
        foreach ($tails as $tail) {
            $result[] = array_merge($sourceItem, [$tail]);
        }
    }

    return $result;
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...