次のような配列があるとします。
Array
(
[arm] => Array
(
[0] => A
[1] => B
[2] => C
)
[gender] => Array
(
[0] => Female
[1] => Male
)
[location] => Array
(
[0] => Vancouver
[1] => Calgary
)
)
外部の連想配列のキーを保持し、内部の連想配列でそれらを使用しながら、どのようにしてデカルト積を見つけることができますか?アルゴリズムの結果は次のようになります。
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.
私は多くのデカルト積アルゴリズムを調べましたが、連想キーを保存する方法の詳細に行き詰まっています。私が使用している現在のアルゴリズムは、数値インデックスのみを提供します。
$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);
任意の助けいただければ幸いです。
ここに私が示すことを恥ずかしくない解決策があります。
あなたの例のように、N
サブ配列を持つ入力配列_$input
_があるとします。各サブ配列にはCn
項目があります。ここで、n
は_$input
_内のインデックスであり、そのキーはKn
です。 i
thサブ配列のn
thアイテムを_Vn,i
_として参照します。
以下のアルゴリズムは、誘導によって機能することを証明できます(バグがない場合)。
1)N = 1の場合、デカルト積は単にarray(0 => array(K1 => V1,1), 1 => array(K1 => V1,2), ... )
-合計でC1アイテムです。これは単純なforeach
で実行できます。
2)_$result
_がすでに最初のN-1サブ配列のデカルト積を保持していると仮定します。 _$result
_のデカルト積とN番目のサブ配列は、次の方法で生成できます。
3)_$product
_内の各項目(配列)に、値_KN => VN,1
_を追加します。結果として得られたアイテム(付加価値付き)を思い出してください。これを_$item
_と呼びます。
4a)_$product
_内の各配列:
4b)セット_VN,2 ... VN,CN
_の各値について、_$product
_に_$item
_のコピーを追加しますが、キーKN
を使用して値を_VN,m
_に変更します(すべての_2 <= m <= CN
_)。
2つの反復4a(_$product
_を介して)および4b(N番目の入力サブ配列を介して)は、_$result
_で終了し、反復前のすべての項目に対してCN
項目が含まれます。最後の_$result
_には、最初のN個のサブ配列のデカルト積が含まれています。
したがって、アルゴリズムはどのNでも機能します。
これは本来あるべきものより書くのが難しかった。私の正式な証明は間違いなく錆びてきている...
_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));
_
@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;
}
このアルゴリズムの背後にある数学の詳細をお読みください: http://en.wikipedia.org/wiki/Cartesian_product
さまざまな言語でこのアルゴリズムの例をさらにご覧ください: https://rosettacode.org/wiki/Cartesian_product_of_two_or_more_lists
PHP 7では、@ Sergの答えは次のように短縮できます。
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;
}
ここに私が思いつくことができるものがあります:
_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);
}
_
(PHPは、そのようなことをするには冗長すぎます。)なので、以下の擬似配列/リスト/辞書表記を使用します。
inject
関数は_a, [b]
_を[(a,b)]
に変換します。つまり、単一の値を配列の各値に挿入し、配列の配列を返します。 a
またはb
がすでに配列であるかどうかは関係ありません。常に2次元配列を返します。
_inject('a', ['foo', 'bar'])
=> [('a', 'foo'), ('b', 'bar')]
_
Zip
関数は、inject
関数を配列の各要素に適用します。
_Zip(['a', 'b'], ['foo', 'bar'])
=> [('a', 'foo'), ('a', 'bar'), ('b', 'foo'), ('b', 'bar')]
_
これは実際にはデカルト積を生成するため、Zip
はわずかな誤称です。この関数をデータセットのすべての要素に連続して適用するだけで、任意の長さの配列のデカルト積が得られます。
_Zip(zip(['a', 'b'], ['foo', 'bar']), ['42', '76'])
=> [('a', 'foo', '42'), ('a', 'foo', '76'), ('a', 'bar', '42'), …]
_
これにはキーは含まれていませんが、要素はすべて結果セット内で順番に並んでいるため、キーを結果に再挿入するだけで済みます。
_array_combine(['key1', 'key2', 'key3'], ['a', 'foo', '42'])
=> [ key1 : 'a', key2 : 'foo', key3 : '42' ]
_
これを製品のすべての要素に適用すると、望ましい結果が得られます。
必要に応じて、上記の3つの関数を1つの長いステートメントに折りたたむことができます(これにより、誤称も解消されます)。
PHP <= 5.2の匿名関数なしの「アンロール」バージョンは、次のようになります。
_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;
}
_
なぜ再帰ジェネレータを使用しないのですか...メモリの問題:なし
(そしてそれは美しい)
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[];
}
注:これはキーを保持しません。しかし、それは始まりです。
これは行う必要があります(テストされていません):
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[];
}
別の解決策:
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;
}
使用法:
$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));
私はすぐにあなたのコードを少し調整しました、私の試みは粗雑だと思いますが、これがあなたの望むように機能するかどうか見てください:
$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);
メモリの消費が重要な場合、または最終的にすべての組み合わせが必要ない場合は、イテレータを使用して一度に1つの組み合わせを生成できます。すべての組み合わせが必要な場合は、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;
}
次に、一度に1つのソリューションを取得するには、次のようにforeach
またはnext
を使用できます。
$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);
数個の組み合わせだけが必要な場合、このソリューションは非常に高速です。また、メモリ消費量は非常に少なくなります(フラットarray
を使用して、一部のintegers
を格納します)。
注:再帰関数は使用されません。
これを行うためにデータベースを使用しないのはなぜですか?
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)
次に、クエリを実行します
$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) )
{
....
}
そしてそれを読んでください:
1つのアルゴリズムは、各ステップで前の結果を現在のステップアイテムで展開することです。
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;
}
このソリューションは、メモリを使用してすべての組み合わせを格納し、それらを一度に返します。つまり、高速ですが、大量のメモリを必要とします。また、再帰関数は使用されません。