Trouver un produit cartésien avec des tableaux associatifs PHP

Dire que j'ai un tableau comme le suivant:

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

Comment puis-je trouver le produit cartésien tout en conservant les clés du réseau associatif externe et en les utilisant dans le réseau interne? Le résultat de l'algorithme devrait être:

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.

j'ai cherché un certain nombre d'algorithmes de produits cartésiens mais je suis coincé sur les détails de la façon de préserver les clés associatives. L'algorithme actuel que j'utilise donne indices numériques seulement:

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

Toute aide serait appréciée.

43
demandé sur Lotus Notes 2011-06-11 00:35:18

10 réponses

Voici une solution que je n'aurais pas honte de montrer.

"1519310920 la" Justification

supposons que nous avons un tableau d'entrée $input avec des sous-tableaux N , comme dans votre exemple. Chacun sous-tableau a Cn articles, où n est son index à l'intérieur de $input , et sa clé est Kn . Je vais me référer au i e élément du n e sous-tableau comme Vn,i .

l'algorithme ci-dessous peut être prouvé à travailler (sauf les bogues) par induction:

1) pour N = 1, Le produit cartésien est simplement array(0 => array(K1 => V1,1), 1 => array(K1 => V1,2), ... ) -- C1 éléments au total. Cela peut être fait avec un simple foreach .

2) supposons que $result contient déjà le produit cartésien des premiers sous-tableaux N-1. Le produit cartésien de $result et le sous-tableau Nth peuvent être produits de cette façon:

3) dans chaque élément (tableau) à l'intérieur $product , ajouter la valeur KN => VN,1 . Rappelez-vous le résultat (avec la valeur ajoutée); Je l'appellerai $item .

4a) pour chaque tableau à l'intérieur de $product :

4b) pour chaque valeur de l'ensemble VN,2 ... VN,CN , ajouter à $product une copie de $item , mais changer la valeur avec la touche KN en VN,m (pour tous 2 <= m <= CN ).

les deux itérations 4a (au-dessus de $product ) et 4b (au-dessus du sous-tableau d'entrée Nth) se termine avec $result ayant CN articles pour chaque article qu'il avait avant les itérations, donc à la fin $result contient en effet le produit cartésien de la première N sous-tableaux.

par conséquent l'algorithme fonctionnera pour n'importe quel N.

c'était plus difficile à écrire qu'il n'aurait dû l'être. Mes preuves formelles sont certainement rouillé...

Code

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

Utilisation

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

print_r(cartesian($input));

de les Voir en action!

50
répondu Jon 2011-06-11 01:12:54

Voici la version optimisée de la fonction cartésienne 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;
}

plus d'informations sur les mathématiques derrière cet algorithme: http://en.wikipedia.org/wiki/Cartesian_product

voir d'autres exemples de cet algorithme en différentes langues: https://rosettacode.org/wiki/Cartesian_product_of_two_or_more_lists

33
répondu Sergiy Sokolenko 2018-07-20 20:35:49

voici ce que je pourrais trouver:

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

(utiliser la notation pseudo array/list/dictionary ci-dessous car PHP est tout simplement trop verbeux pour de telles choses.)

la fonction inject transforme a, [b] en [(a,b)] , c'est-à-dire qu'elle injecte une seule valeur dans chaque valeur d'un tableau, retournant un tableau de tableaux. Peu importe si a ou b c'est déjà un tableau, il retournera toujours un tableau en deux dimensions.

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

la fonction zip applique la fonction inject à chaque élément d'un tableau.

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

notez que cela produit en fait un produit cartésien, donc zip est un nom légèrement erroné. En appliquant simplement cette fonction à tous les éléments d'un ensemble de données, vous obtenez le produit cartésien pour un tableau de n'importe quelle longueur.

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

cela ne contient pas les clés, mais puisque les éléments sont tous dans l'ordre dans le jeu de résultats, vous pouvez simplement réinjecter les clés dans le résultat.

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

appliquer ceci à tous les éléments du produit donne le résultat désiré.

vous pouvez décomposer les trois fonctions ci-dessus en une seule longue déclaration si vous le souhaitez (qui éclaircirait également les misnomers).


une version "déroulante" sans fonctions anonymes pour PHP < = 5.2 ressemblerait à ceci:

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
répondu deceze 2011-06-12 04:01:13

pourquoi ne pas utiliser un générateur récursif ... problèmes de mémoire: proche de aucun

(et de sa belle)

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

note: Ceci ne préserve pas les clés; mais c'est un début.

cela doit faire (non testé):

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
répondu Titus 2016-12-22 12:25:34

une autre solution:

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

Utilisation:

$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
répondu Respant 2013-04-13 12:12:07

en PHP 7 la réponse de @Serg peut être raccourcie à:

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
répondu freytag 2017-07-04 16:09:09

j'ai rapidement ajusté votre code un peu , ma tentative est grossière je pense mais voir si cela fonctionne comme vous voulez:

$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
répondu Sabeen Malik 2011-06-10 21:36:02

pourquoi ne pas utiliser une base de données pour faire cela?

C'est facile à 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)

puis faire une requête

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

et lire:

1
répondu Johan 2011-06-10 21:15:58

si la consommation de mémoire est importante ou si vous n'avez pas besoin de toutes les combinaisons à la fin, vous pouvez utiliser un itérateur pour générer une combinaison à la fois. Si vous avez besoin de toutes les combinaisons vous pouvez utiliser 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;
}

alors, pour obtenir une solution à la fois , vous pourriez utiliser un foreach ou next , comme ceci:

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

cette solution est très rapide si vous n'avez besoin que de quelques combinaisons. Aussi, la mémoire la consommation est très faible (il utilise un plat array à conserver integers ).

Note: les fonctions récursives ne sont pas utilisées.

1
répondu Constantin Galbenu 2017-04-30 07:15:32

Un algorithme est d'élargir à chaque étape les résultats précédents avec l'étape en cours:

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

cette solution utilise la mémoire pour stocker toutes les combinaisons puis les retourne toutes en même temps. Donc, c'est rapide, mais il a besoin de beaucoup de mémoire. Aussi, fonctions récursives ne sont pas utilisés.

0
répondu Constantin Galbenu 2017-04-30 07:08:45