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.
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é...
Codefunction 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!
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
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;
}
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[];
}
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));
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;
}
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);
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:
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.
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.