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.
Voici une solution que je n'aurais pas honte de montrer.
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 ...
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));
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
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;
}
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;
}
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[];
}
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));
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);
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.
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:
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.