web-dev-qa-db-fra.com

Trouver des produits cartésiens avec PHP Landises associatives

Dis que j'ai un tableau comme ce qui suit:

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 préservant les clés du tableau externe associatif et en les utilisant dans les internes? Le résultat de l'algorithme devrait être ceci:

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 regardé un certain nombre d'algorithmes de produits cartésiens, mais je suis coincé sur les détails de la manière de préserver les clés associatives. L'algorithme actuel que j'utilise donne des indices numériques:

    $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.

50
Lotus Notes

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

Raisonnement

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

L'algorithme ci-dessous peut être prouvé de fonctionner (sauf bugs) par induction:

1) Pour n = 1, le produit cartésien est simplement array(0 => array(K1 => V1,1), 1 => array(K1 => V1,2), ... ) - C1 articles au total. Cela peut être fait avec un simple foreach.

2) Supposons que $result contient déjà le produit cartésien des premiers bancs de N-1. Le produit cartésien de $result et de la nième sous-tableau peuvent être produits de cette façon:

3) Dans chaque élément (tableau) à l'intérieur $product, ajoutez la valeur KN => VN,1. Rappelez-vous l'élément résultant (avec la valeur ajoutée); Je vais me référer à $item.

4a) Pour chaque matrice à l'intérieur $product:

4b) Pour chaque valeur dans l'ensemble VN,2 ... VN,CN, ajoutez à $product une copie de $item, mais modifiez la valeur avec la touche KN sur VN,m (pour tout 2 <= m <= CN).

Les deux itérations 4a (sur $product) et 4b (sur la nième sous-tableau d'entrée) se termine avec $result ayant des éléments CN pour chaque article qu'il avait avant l'itération, donc à la fin $result contient en effet le produit cartésien des premiers n sous-tableaux.

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

C'était plus difficile à écrire qu'elle aurait dû l'avoir été. Mes preuves formelles sont définitivement en train de devenir rouillées ...

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

Usage

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

print_r(cartesian($input));
54
Jon

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

En savoir plus sur les mathématiques derrière cet algorithme: http://fr.wikipedia.org/wiki/cartesian_product

En voir plus Exemples de cet algorithme dans différentes langues: https://rosettacode.org/wiki/cartesian_product_of_two_or_more_lists

37
Sergiy Sokolenko

IN PHP 7 @ Réponse de Serg peut être raccourci pour:

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
freytag

Voici ce que je pouvais 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);
}

(Utilisation de la notation Pseudo Array/List/Dictionnaire ci-dessous depuis PHP est simplement trop verbeuse pour de telles choses.

La fonction inject transforme a, [b] En [(a,b)], c'est-à-dire injectant une valeur unique dans chaque valeur d'un tableau, renvoyant un tableau de réseaux. Peu importe que a ou b 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'une matrice.

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

Notez que cela produit réellement un produit cartésien, alors Zip est un léger dénomineur. Il suffit d'appliquer cette fonction à tous les éléments d'un ensemble de données en succession vous donne le produit cartésien pour un tableau de toute longueur.

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

Cela ne contient pas les touches, mais étant donné que 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' ]

L'application de cela à tous les éléments du produit donne le résultat souhaité.

Vous pouvez réduire les trois fonctions ci-dessus dans une seule longue déclaration si vous le souhaitez (ce qui effacerait également les problèmes d'inutilisation).


Une version "déroulée" 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
deceze

Pourquoi ne pas utiliser de générateur récursif ... Problèmes de mémoire: Proche de Aucun
[.____] (et c'est beau)

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

remarque: cela ne préserve pas les clés; Mais c'est un début.

Cela devrait 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[];
}
6
Titus

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

tilisation :

$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
Respant

J'ai rapidement ajusté votre code un peu, ma tentative est crude, je pense mais voir si cela fonctionne comme vous le souhaitez:

$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
Sabeen Malik

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

Ensuite, pour obtenir une solution à la fois, vous pouvez utiliser un foreach ou next, comme celui-ci:

$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 très rapide si vous n'avez besoin que de quelques combinaisons. En outre, la consommation de mémoire est très faible (il utilise un appartement array pour stocker certains integers).

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

1
Constantin Galbenu

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

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

Alors faites 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 lisez ça:

1
Johan

Un algorithme est de développer à chaque étape les résultats précédents avec les éléments de l'étape actuels:

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 les combinaisons toutes les combinaisons, puis les renvoie tous à la fois. Donc, c'est rapide mais cela a besoin de beaucoup de mémoire. En outre, les fonctions récursives ne sont pas utilisées.

0
Constantin Galbenu