Encontrar productos cartesianos con arrays asociativos PHP


Digamos que tengo una matriz como la siguiente:

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

¿Cómo puedo encontrar el producto cartesiano conservando las claves de la matriz asociativa externa y usándolas en las internas? El resultado del algoritmo debe ser el siguiente:

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.

He buscado un buen número de algoritmos de productos cartesianos, pero me estoy quedando atascado en los detalles de cómo preservar las claves asociativas. El algoritmo actual que estoy usando solo da índices numéricos:

    $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);

Cualquier ayuda se agradecería.

Author: Lotus Notes, 2011-06-11

10 answers

Aquí hay una solución que no me avergonzaría de mostrar.

Justificación

Supongamos que tenemos una matriz de entrada $input con sub-matrices N, como en su ejemplo. Cada sub-array tiene elementos Cn, donde n es su índice dentro de $input, y su clave es Kn. Me referiré al ith elemento del nth sub-array como Vn,i.

Se puede demostrar que el algoritmo de abajo funciona (salvo errores) por inducción:

1) Para N = 1, el producto cartesiano es simplemente array(0 => array(K1 => V1,1), 1 => array(K1 => V1,2), ... ) C C1 artículos en total. Esto se puede hacer con un simple foreach.

2) Supongamos que $result ya contiene el producto cartesiano de los primeros subarrays N-1. El producto cartesiano de $result y el enésimo sub-array se pueden producir de esta manera:

3) En cada elemento (matriz) dentro de $product, agregue el valor KN => VN,1. Recuerde el elemento resultante (con el valor añadido); me referiré a él como $item.

4a) Para cada matriz dentro de $product:

4b) Para cada valor en el conjunto VN,2 ... VN,CN, añadir a $product una copia de $item, pero cambie el valor con la clave KN a VN,m (para todos 2 <= m <= CN).

Las dos iteraciones 4a (sobre $product) y 4b (sobre la Enésima sub-matriz de entrada) terminan con $result teniendo CN elementos para cada elemento que tenía antes de las iteraciones, por lo que al final $result de hecho contiene el producto cartesiano de los primeros N sub-matrices.

Por lo tanto, el algoritmo funcionará para cualquier N.

Esto fue más difícil de escribir de lo que debería haber sido. Mis pruebas definitivamente se están oxidando...

Código

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;
}

Uso

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

print_r(cartesian($input));

¡Véalo en acción!

 50
Author: Jon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-11 01:12:54

Aquí está la versión optimizada de la función cartesiana de @Jon:

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;
}

Leer más sobre las matemáticas detrás de este algoritmo: http://en.wikipedia.org/wiki/Cartesian_product

Ver más ejemplos de este algoritmo en diferentes idiomas: https://rosettacode.org/wiki/Cartesian_product_of_two_or_more_lists

 33
Author: Sergiy Sokolenko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-20 20:35:49

Esto es lo que se me ocurrió:{[15]]}

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);
}

(Usando pseudo array/list / dictionary notation a continuación ya que PHP es simplemente demasiado detallado para tales cosas.)

La función inject transforma a, [b] en [(a,b)], es decir, inyecta un solo valor en cada valor de una matriz, devolviendo una matriz de matrices. No importa si a o b ya es una matriz, siempre devolverá una matriz bidimensional.

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

La función zip aplica la función inject función a cada elemento en una matriz.

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

Tenga en cuenta que esto en realidad produce un producto cartesiano, por lo que zip es un ligero nombre incorrecto. Simplemente aplicando esta función a todos los elementos en un conjunto de datos en sucesión le da el producto cartesiano para una matriz de cualquier longitud.

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

Esto no contiene las claves, pero como los elementos están todos en orden dentro del conjunto de resultados, simplemente puede volver a inyectar las claves en el resultado.

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

Aplicando esto a todos los elementos en el producto da el resultado deseado.

Puede contraer las tres funciones anteriores en una sola declaración larga si lo desea (lo que también aclararía los nombres incorrectos).


Una versión "desenrollada" sin funciones anónimas para PHP

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;
}
 7
Author: deceze,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-12 04:01:13

¿Por qué no usar un generador recursivo?.. problemas de memoria: cerca de ninguno
(y su hermoso)

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[];
}

Nota: esto no conserva las claves; pero es un comienzo.

Esto debe hacer (no probado):

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[];
}
 5
Author: Titus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-22 12:25:34

Otra solución:

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;
}

Uso:

$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));
 3
Author: Respant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-13 12:12:07

En PHP 7 @ Serg la respuesta se puede acortar a:

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;
}
 3
Author: freytag,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-04 16:09:09

Ajusté rápidamente tu código un poco, mi intento es crudo, creo, pero mira si esto funciona como quieres:

$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);
 2
Author: Sabeen Malik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-10 21:36:02

¿Por qué no usar una base de datos para hacer esto?

Es fácil en 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)

Luego haga una consulta

$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) )
{
   ....
}

Y léalo en voz alta:

 1
Author: Johan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-10 21:15:58

Si el consumo de memoria es importante o no necesita todas las combinaciones al final, podría usar un iterador para generar una combinación a la vez. Si necesitas todas las combinaciones puedes usar 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;
}

Entonces, para obtener una solución a la vez, podría usar un foreach o next, como este:

$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);

Esta solución es muy muy rápida si solo necesita unas pocas combinaciones. Además, el consumo de memoria es muy bajo (utiliza un plano array para almacenar algunos integers).

Nota: no se utilizan funciones recursivas.

 1
Author: Constantin Galbenu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-30 07:15:32

Un algoritmo es expandir en cada paso los resultados anteriores con los elementos del paso actual:

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;
}

Esta solución usa memoria para almacenar todas las combinaciones y luego las devuelve todas a la vez. Es rápido, pero necesita mucha memoria. Además, no se utilizan funciones recursivas.

 0
Author: Constantin Galbenu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-30 07:08:45