What is the most efficient way to generate all the combinations, dispositions and permutations of an array in PHP?
Answers
Nesting your loops is the trick. Try the following example. I've replaced your while-loops by foreach-loops with the PHP range function, and nested (i.e. loop-inside-a-loop) them inside eachother:
function colourArray($number) {
$colours = array();
foreach(range(0,$number) as $r) {
foreach(range(0,$number) as $g) {
foreach(range(0,$number) as $b) {
$colours[] = array($r,$g,$b);
}
}
}
return $colours;
}
References:
http://php.net/range
http://php.net/manual/en/control-structures.foreach.php
Here's a solution I wouldn't be ashamed to show.
Rationale
Assume that we have an input array $input
with N
sub-arrays, as in your example. Each
sub-array has Cn
items, where n
is its index inside $input
, and its key is Kn
. I will refer to the i
th item of the n
th sub-array as Vn,i
.
The algorithm below can be proved to work (barring bugs) by induction:
1) For N = 1, the cartesian product is simply array(0 => array(K1 => V1,1), 1 => array(K1 => V1,2), ... )
-- C1 items in total. This can be done with a simple foreach
.
2) Assume that $result
already holds the cartesian product of the first N-1 sub-arrays. The cartesian product of $result
and the Nth sub-array can be produced this way:
3) In each item (array) inside $product
, add the value KN => VN,1
. Remember the resulting item (with the added value); I 'll refer to it as $item
.
4a) For each array inside $product
:
4b) For each value in the set VN,2 ... VN,CN
, add to $product
a copy of $item
, but change the value with the key KN
to VN,m
(for all 2 <= m <= CN
).
The two iterations 4a (over $product
) and 4b (over the Nth input sub-array) ends up with $result
having CN
items for every item it had before the iterations, so in the end $result
indeed contains the cartesian product of the first N sub arrays.
Therefore the algorithm will work for any N.
This was harder to write than it should have been. My formal proofs are definitely getting rusty...
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));
If you want O(n) time and no extra memory usage (since array was specified), use the algorithm from Jon Bentley's book, "Programming Pearls 2nd Edition". It swaps all the elements twice. Not as fast as using linked lists but uses less memory and is conceptually simple.
shiftArray( theArray, M ):
size = len( theArray )
assert( size > M )
reverseArray( theArray, 0, size - 1 )
reverseArray( theArray, 0, M - 1 )
reverseArray( theArray, M, size - 1 )
reverseArray( anArray, startIndex, endIndex ) reverses the order of elements from startIndex to endIndex, inclusive.
I noticed that your updated getPerms function contains duplicates. Here's my crack at a dupe-free version. Hopefully the comments speak for themselves. The hardest part was writing an efficient distrib
function, because the concatenation operator has to be used somewhere. Luckily it's only used on small sublists, so the performance remains reasonable. My getAllPerms code below generates all permutations of [1..9] in around a quarter of a second, all 10-element permutations in around 2.5 seconds.
Edit: funny, I didn't look at Tomas' code, but his combinations function and my picks function are nearly identical.
// All ordered picks {x_i1, x_i2, .. , x_ik} of k out of n elements {x_1,..,x_n}
// where i1 < i2 < .. < ik
let picks n L =
let rec aux nleft acc L = seq {
match nleft,L with
| 0,_ -> yield acc
| _,[] -> ()
| nleft,h::t -> yield! aux (nleft-1) (h::acc) t
yield! aux nleft acc t }
aux n [] L
// Distribute an element y over a list:
// {x1,..,xn} --> {y,x1,..,xn}, {x1,y,x2,..,xn}, .. , {x1,..,xn,y}
let distrib y L =
let rec aux pre post = seq {
match post with
| [] -> yield (L @ [y])
| h::t -> yield (pre @ y::post)
yield! aux (pre @ [h]) t }
aux [] L
// All permutations of a single list = the head of a list distributed
// over all permutations of its tail
let rec getAllPerms = function
| [] -> Seq.singleton []
| h::t -> getAllPerms t |> Seq.collect (distrib h)
// All k-element permutations out of n elements =
// all permutations of all ordered picks of length k combined
let getPerms2 n lst = picks n lst |> Seq.collect getAllPerms
Edit: more code in response to comments
// Generates the cartesian outer product of a list of sequences LL
let rec outerProduct = function
| [] -> Seq.singleton []
| L::Ls -> L |> Seq.collect (fun x ->
outerProduct Ls |> Seq.map (fun L -> x::L))
// Generates all n-element combination from a list L
let getPermsWithRep2 n L =
List.replicate n L |> outerProduct
Here is code to get all permutations:
http://php.net/manual/en/function.shuffle.php#90615
With the code to get the power set, permutations are those of maximal length, the power set should be all combinations. I have no idea what dispositions are, so if you can explain them, that would help.