从多个值列表中查找所有不冲突的值组合

Posted

技术标签:

【中文标题】从多个值列表中查找所有不冲突的值组合【英文标题】:Finding all non-conflicting combinations of values from multiple lists of values 【发布时间】:2010-11-25 13:45:08 【问题描述】:

我有以下包含值数组的数组:

$array = array(
    array('1', '2'),
    array('a', 'b', 'c'),
    array('x', 'y'),
);

可以有任意数量的数组,并且一个数组可以包含任意数量的值。我目前有一段代码将生成所有组合,其中从每个数组中获取一个值。例如:

1ax, 1ay, 1bx, 1by, 1cx, 1cy, 2ax, 2ay, 2bx, 2by, 2cx, 2cy

但是,我真正想要的只是每列中只有一个值的组合,即。 1ax 不好,因为所有三个值 1、a 和 x 都位于第一列,1by 不好,因为 b 和 y 位于第二列。所以从上面的例子中只有这些组合是有效的:

1cy, 2cx

我最初计划只生成所有组合,然后过滤掉有冲突的组合,但这并不能扩展,因为这是一个过于简单的示例,在实际应用中可能会有数百万个组合的情况(包括冲突的)。

谁能提供更好的方法来解决这个问题?我正在使用 php,但任何清楚地展示逻辑的代码示例都会有所帮助。

提前致谢。


更新:

我已经测试了适用于更大数据集的解决方案,以获得一些基准,这些是迄今为止的结果:

$array = array(
    array('1', '2', '3', '1', '2', '3', '1', '2', '3', '1', '2', '3', '1', '2', '3'),
    array('a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd'),
    array('x', 'y', 'z', 'x', 'y', 'z', 'x', 'y', 'z'),
    array('1', '2', '3', '1', '2', '3', '1', '2', '3'),
    array('a', 'b', 'c', 'd', 'a', 'b', 'c', 'd', 'a', 'b', 'c', 'd'),
    array('x', 'y', 'z'),
);

Josh Davis 第二个解决方案:

Combinations:      249480
Time:              0.3180251121521 secs
Memory Usage:      22.012168884277 mb
Peak Memory Usage: 22.03059387207 mb

乔什·戴维斯:

Combinations:      249480
Time:              1.1172790527344 secs
Memory Usage:      22.004837036133 mb
Peak Memory Usage: 22.017387390137 mb

汤姆·黑格:

Combinations:      249480
Time:              5.7098741531372 secs
Memory Usage:      39.145843505859 mb
Peak Memory Usage: 39.145843505859 mb

【问题讨论】:

我已经用另一种更快的算法更新了我的答案。 【参考方案1】:

有趣的问题!事实证明这比我想象的要复杂,但它似乎有效。

基本策略是先将数组从小到大排序(跟踪它们的顺序,以便我可以按正确的顺序输出答案)。

我将答案以索引数组的形式保存到这个排序的输入列表数组中。

现在列表已排序,我可以将第一个正确答案存储为 array(0,1,2,...,n);

然后我递归到一个函数,通过将其与该答案数组中的其他值交换(对于该插槽来说不是太大的所有值)来尝试那里的第一个插槽(上面的 0)中的所有值。由于我已按大小对其进行排序,因此我可以在交换时将任何值向右移动,而不必担心它对于那个正确的插槽来说太大了。

输出每个有效槽都有一些疯狂的间接来撤消所有排序。

对不起,如果这看起来令人困惑。我没有花太多时间清理它。

<?php
# $lists is an array of arrays
function noconfcombos($lists) 
    $lengths = array();
    foreach($lists as $list) 
        $lengths[] = count($list);
    

    # find one solution (and make sure there is one)
    $answer = array();
    $sorted_lengths = $lengths;
    asort($sorted_lengths);
    $answer_order_lists = array();
    $answer_order_lengths = array();
    $output_order = array();
    $min = 1;
    $max_list_length = 0;
    foreach($sorted_lengths as $lists_key => $list_max) 
        if($list_max < $min) 
            # no possible combos
            return array();
        
        $answer[] = $min - 1; # min-1 is lowest possible value (handing out colums starting with smallest rows)
        $output_order[$lists_key] = $min - 1; # min-1 is which slot in $answers corresponds to this list
        $answer_order_lists[] = $lists[$lists_key];
        $answer_order_lengths[] = $lengths[$lists_key];
        ++$min;
    
    ksort($output_order);
    $number_of_lists = count($lists);
    $max_list_length = end($sorted_lengths);
    if($max_list_length > $number_of_lists) 
       for($i = $number_of_lists; $i < $max_list_length; ++$i) 
          $answer[] = $i;
       
       $stop_at = $number_of_lists;
     else 
       $stop_at = $number_of_lists - 1;
    

    # now $answer is valid (it has the keys into the arrays in $list for the
    # answer), and we can find the others by swapping around the values in
    # $answer.

    $ret = array();
    $ret[] = noconfcombos_convert($answer, $answer_order_lists, $output_order);
    noconfcombos_recurse($ret, $max_list_length, $stop_at, $answer_order_lengths, $answer_order_lists, $output_order, $answer, 0);

    return $ret;


# try swapping in different indexes into position $index, from the positions
# higher, then recurse
function noconfcombos_recurse(&$ret, $max_list_length, $stop_at, &$lengths, &$lists, &$output_order, $answer, $index) 
    if($index < $stop_at) 
        noconfcombos_recurse($ret, $max_list_length, $stop_at, $lengths, $lists, $output_order, $answer, $index + 1);
    
    for($other = $index + 1; $other < $max_list_length; ++$other) 
        if($answer[$other] < $lengths[$index])  # && $answer[$index] < $lengths[$other]) 
            $tmp = $answer[$index];
            $answer[$index] = $answer[$other];
            $answer[$other] = $tmp;
            $ret[] = noconfcombos_convert($answer, $lists, $output_order);
            if($index < $stop_at) 
                noconfcombos_recurse($ret, $max_list_length, $stop_at, $lengths, $lists, $output_order, $answer, $index + 1);
            
        
    



function noconfcombos_convert(&$indexes, &$lists, &$order) 
    $ret = '';
    foreach($order as $i) 
        $ret .= $lists[$i][$indexes[$i]];
    
    return $ret;


function noconfcombos_test() 
    $a = array('1', '2', '3', '4');
    $b = array('a', 'b', 'c', 'd', 'e');
    $c = array('x', 'y', 'z');
    $all = array($a, $b, $c);
    print_r(noconfcombos($all));


noconfcombos_test();

【讨论】:

当我使用这些数据 [[1, 2, 3], [a, b, c, d], [x, y, z]] 运行它时,我只得到 6 种组合(应该是 12),其中两个是重复的:1cy, 1bz, 3bx, 3ay, 1cy, 1bz 哎呀。好的,一个愚蠢的错误:我不应该通过引用传递 $answer。所以从函数 noconfcombos_recurse 定义中的 &$answer 中删除 &。然后下一个问题是,我的代码仅设计用于处理最长列表中的值数量等于列表数量的集合。通过一些额外的代码,它应该可以很好地处理列多于行的集合。 好的,我将使用修复这两个错误的新代码来编辑我的帖子。 好的,所以为了让它处理比行更多的列,我让 $anwsers 变量拥有比实际输出更多的槽。我还必须稍微修改递归代码,以便如果有像这样的额外插槽,它会进一步递归(因为它需要用额外的地方交换最后一个地方,而在没有任何东西可以交换之前)。 【参考方案2】:

这是自生成代码和蛮力将在简单性和性能方面击败大多数算法的情况之一。在以前的答案中,我看到了很多递归、数组操作和计算,而实际上您想要做的是:

foreach ($array[0] as $k0 => $v0)

    foreach ($array[1] as $k1 => $v1)
    
        if ($k1 == $k0)
        
            continue;
        
        foreach ($array[2] as $k2 => $v2)
        
            if ($k2 == $k1 || $k2 == $k0)
            
                continue;
            
            $result[] = $v0.$v1.$v2;
        
    

当然,除非你知道$array 中有多少个数组,否则你不能写这个。这就是生成的代码派上用场的地方:

$array = array(
    array('1', '2'),
    array('a', 'b', 'c'),
    array('x', 'y')
);
$result = array();

$php = '';
foreach ($array as $i => $arr)

    $php .= 'foreach ($array[' . $i . '] as $k' . $i . ' => $v' . $i . ')';

    if ($i > 0)
    
        $sep  = 'if (';
        $j    = $i - 1;
        do
        
            $php .= $sep . '$k' . $i . ' == $k' . $j;
            $sep  = ' || ';
        
        while (--$j >= 0);

        $php .= ')  continue;  ';
    


$php .= '$result[] = $v' . implode('.$v', array_keys($array)) . ';' . str_repeat('', count($array));

eval($php);
print_r($result);

请注意,此例程假定 $array 是一个从零开始的数字索引数组,如您的示例所示。它将生成上面引用的代码,并针对任意数量的数组进行调整。


更新

这是一种替代算法。它仍然是自我生成的,但不那么蛮力。我们仍然有嵌套循环,除了每个循环都在数组的副本上工作,其中外部循环当前使用的键已从该循环的数组中删除。例如,如果值应该是 (a,b,c) 但外部循环使用索引 0 和 2,我们删除“a”(索引 0)和“c”(索引 2),剩下的就是“乙”。这意味着循环仅适用于可能的组合,我们不再需要if 条件。

此外,这部分也可以应用于前面的算法,我们按照从小到大的顺序处理值的数组,以保证当前数组中存在使用的索引。缺点是它不会以相同的顺序生成组合。它生成相同的组合,只是顺序不同。代码如下所示:

$a0 = $array[0];
foreach ($a0 as $k0 => $v0)

    $a2 = $array[2];
    unset($a2[$k0]);
    foreach ($a2 as $k2 => $v2)
    
        $a1 = $array[1];
        unset($a1[$k0], $a1[$k2]);
        foreach ($a1 as $k1 => $v1)
        
            $result[] = "$v0$v1$v2";
        
    

上述例程在每个循环开始时设置值的副本,然后删除外部循环使用的值。您可以通过在开始时设置值的副本仅一次来改进此过程,在使用时删除键(在每个循环的开头)并在释放时将它们放回原处(在每个循环结束时)。然后例程如下所示:

list($a0,$a1,$a2) = $array;
foreach ($a0 as $k0 => $v0)

    unset($a1[$k0], $a2[$k0]);
    foreach ($a2 as $k2 => $v2)
    
        unset($a1[$k2]);
        foreach ($a1 as $k1 => $v1)
        
            $result[] = "$v0$v1$v2";
        
        $a1[$k2] = $array[1][$k2];
    
    $a1[$k0] = $array[1][$k0];
    $a2[$k0] = $array[2][$k0];

上面生成源码的实际代码是:

$keys = array_map('count', $array);
arsort($keys);

$inner_keys = array();
foreach ($keys as $k => $cnt)

    $keys[$k] = $inner_keys;
    $inner_keys[] = $k;


$result = array();

$php = 'list($a' . implode(',$a', array_keys($array)) . ')=$array;';
foreach (array_reverse($keys, true) as $i => $inner_keys)

    $php .= 'foreach ($a' . $i . ' as $k' . $i . ' => $v' . $i . ')';

    if ($inner_keys)
    
        $php .= 'unset($a' . implode('[$k' . $i . '],$a', $inner_keys) . '[$k' . $i . ']);';
    


$php .= '$result[] = "$v' . implode('$v', array_keys($array)) . '";';

foreach ($keys as $i => $inner_keys)

    foreach ($inner_keys as $j)
    
        $php .= '$a' . $j . '[$k' . $i . ']=$array[' . $j . '][$k' . $i . "];\n";
    
    $php .= '';

eval($php);

【讨论】:

+1。这看起来很有希望,但对于任何合理数量的数组(项目)来说,在实践中可能(也)会减慢速度。我使用的 compiled 代码目前正在评估 10 个长度为 10-19 的数组。在不增加第一个数组的索引的情况下,已经找到了 234.783.155 个解决方案。 必须有一种比暴力破解所有组合更好的方法。 在 PHP 中,你很难击败那种算法,因为它不使用任何函数,只依赖于语言结构。如果我没记错的话,长度为 10 到 19 的 10 个数组会产生 100 亿个结果,因此您需要超过 100GB 的 RAM 才能将它们保存在内存中。 您对性能的看法可能是对的,我不了解 PHP。您对内存消耗的看法是非常正确的,我并没有考虑到这一点。我很好奇,100亿的结果你是怎么计算出来的? 如果第一个数组有 10 个元素,最后一个数组有 19 个元素,从 10 到 19 的 10 个数组你有 10 个键 *(11 个键 - 1 个使用过的键)*(12 个键 - 2 个使用过的键) * 等等...你最终得到 10^10 种组合。 太棒了,感谢您提供替代解决方案。现在正在工作,所以稍后会测试它,看起来这将是最好的解决方案。【参考方案3】:

我认为这行得通。它使用递归来像树一样遍历结构。对于每个分支,它会跟踪哪些列已经被占用。它可能很慢,因为它是一种蛮力方法。

<?php 

$array = array(
    array('1', '2'),
    array('a', 'b', 'c'),
    array('x', 'y'),
);


function f($array, & $result, $colsToIgnore = array(), $currentPath = array()) 
    $row = array_shift($array);
    $length = count($row);
    $isMoreRows = !! count($array);

    for ($col = 0; $col < $length; $col++) 
        //check whether column has already been used
        if (in_array($col, $colsToIgnore)) 
            continue;   
        

        $item = $row[$col];

        $tmpPath = $currentPath;
        $tmpPath[] = $item;

        if ($isMoreRows) 
            $tmpIgnoreCols = $colsToIgnore;
            $tmpIgnoreCols[] = $col;
            f($array, $result, $tmpIgnoreCols, $tmpPath);
         else 
            $result[] = implode('', $tmpPath);
        

    



$result = array();
f($array, $result);
print_r($result);

【讨论】:

【参考方案4】:

可能不是最优雅的方式,但可以解决问题 (javascript)

var result = [];

for(i=0;i<arr1.length;i++)

  for(j=0;j<arr2.length;j++)
  
    if(j==i)
      continue;
    else
    
      for(k=0;k<arr3.length;k++)
      
        if(k==i||k==j)
          continue;
        else
        
          result.push(arr1[i]+arr2[j]+arr3[k]);
        
      
    
  

【讨论】:

这只适用于预定义数量的数组 (3),但可以有任意数量的数组。【参考方案5】:

这可以使用递归进行重构,使其适用于任意数量的数组。如果我有时间,我会自己试一试。

ps。不懂php,例子是用Delphi写的。

编辑:任意#数组的递归解

type
  TSingleArray = array of string;
  TMasterArray = array of TSingleArray;
var
  solutions: array of integer; // Q&D container to hold currently used indexes of SingleArrays


procedure WriteSolution(const masterArray: TMasterArray);
var
  I: Integer;
  indexes: string;
  solution: string;
begin
  for I := 0 to High(solutions) do
  begin
    indexes := indexes + IntToStr(solutions[I]) + ' ';
    solution := solution + masterArray[I][solutions[I]];
  end;
  Writeln('Solution: ' + solution + ' Using indexes: ' + indexes);
end;

procedure FindSolution(const masterArray: TMasterArray; const singleArrayIndex: Integer; var bits: Integer);
var
  I: Integer;
begin
  for I := 0 to High(masterArray[singleArrayIndex]) do
  begin
    //***** Use bit manipulation to check if current index is already in use
    if bits and (1 shl I)  = (1 shl I ) then continue;
    solutions[singleArrayIndex] := I;
    Inc(bits, 1 shl I);
    //***** If it is not the last array in our masterArray, continue by calling RecursArrays recursivly.
    if singleArrayIndex <> High(masterArray) then FindSolution(masterArray, Succ(singleArrayIndex), bits)
    else WriteSolution(masterArray);
    Dec(bits, 1 shl I);
  end;
end;

//***************
// Initialization
//***************
var
  I, J: Integer;
  bits: Integer;
  singleArrayString: string;
  masterArray: TMasterArray;
begin
  I := 10;
  SetLength(masterArray, I);
  for I := 0 to High(masterArray) do
  begin
    SetLength(masterArray[I], High(masterArray) + Succ(I));
    singleArrayString := EmptyStr;
    for J := 0 to High(masterArray[I]) do
    begin
      masterArray[I][J] := IntToStr(J);
      singleArrayString := singleArrayString + masterArray[I][J];
    end;
    WriteLn(singleArrayString)
  end;
  ReadLn;

  //****** Start solving the problem using recursion
  bits := 0;
  SetLength(solutions, Succ(High(masterArray)));
  FindSolution(masterArray, 0, bits);    
end.

【讨论】:

很高兴看到它可以如何重构,这是我最苦恼的数组的可变数量。 谢谢,我会尝试用 PHP 重写它,虽然我对 Delphi 一点也不熟悉,所以可能需要一段时间 :-) 。再次感谢。【参考方案6】:

从不同的角度来看:为了组成一个结果行,您需要为每一列选择一个值。每个值都应从不同的源行中选取。这个问题被称为“从 M 中挑选 N”,或者在数学上称为 Combination。

这意味着结果行对应于源行索引数组。

您可以通过开始构建这样的索引数组来构建所有可能的选择(伪代码)

function combinations( $source ) 
  if( count( $source ) == 0 ) return $source;
  $result=array();
  // build one row
  foreach( $source as $index=>$value ) 
    $newsource = array_splice( $source, $index, 1 );

    $reduced_combinations=combinations( $newsource  );
    foreach( $reduced_combinations as $reduced_combi ) 
      $newrow=array_unshift( $reduced_combi, $value );
      $result[]=$newrow;
    

  
  return $result;


function retrieve_indices( $indices, $arrays ) 
   $result=array();
   foreach( $indices as $column=>$index ) 
     $result[]=$arrays[$index][$column];
   
   return $result;


$source_arrays = array(
  array( "1", "2", "3" ),
  array( "a", "b", "c" ),
  array( "x", "y", "z" )
);

$index_combinations = combinations( range(0,2) );
$result=array();
foreach( $index_combinations as $combination ) 
  $result[]=retrieve_indices( $combination, $source_arrays );

【讨论】:

我收到“致命错误:在第 8 行调用未定义函数 remove() ...”? 你是对的。我为此创建了$newsources。忘记用了:) 感谢您的帮助,虽然现在它似乎无限递归...? 确实如此。我忘了检查递归的琐碎情况。 该代码旨在作为一个概念解决方案,说明这个想法。我承认这就是我没有测试它的原因。【参考方案7】:

另一种选择:

    $arr = array(
        array('1', '2'),
        array('a', 'b', 'c'),
        array('x', 'y'),
    );
    //-----
    //assuming $arr consists of non empty sub-arrays
    function array_combinations($arr) 
        $max = 1;
        for ($i = 0; $i < count($arr); $i ++)
            $max *= count($arr[$i]); 
        
        $matrix = array();
        for ($i = 0; $i < $max; $i ++)
            $matrix = array(); 
        
        $c_rep = 1;
        for ($i = count($arr) - 1; $i >= 0; $i --)
            $c_rep *= ($i < count($arr) - 1)//last sub-array 
                ? count($arr[$i + 1])
                : 1;
            $k = 0; 
            while ($k < $max)
                for ($t = 0; $t < count($arr[$i]); $t ++)
                    for ($j = 0; $j < $c_rep; $j ++)
                        $matrix[$i][$k ++] = $arr[$i][$t];
                    
                   
            
        
        return $matrix;
    
    //-----
    $matrix = array_combinations($arr);

【讨论】:

【参考方案8】:

您的问题与finding a determinant of a matrix 的问题类似。恕我直言,最好的方法是用一些符号填充较小的数组,比如'0',所以在你的例子中它们都有相同数量的值

$array = array(
    array('1', '2', '0'),
    array('a', 'b', 'c'),
    array('x', 'y', '0'),
);

然后循环遍历每个第一个数组值,并为每个数组的索引增加 1 并检查下一个数组和下一列(在第一个循环中它将是 '1' 并且索引将是 0 递增 - 1 ,然后得到 $array1 - 'b' 等等)如果你达到'0',打破,如果你到达右边界,将第一个索引重置为0。然后对递减做同样的事情,你将拥有所有组合。可能不清楚,请查看我链接到的图像

【讨论】:

【参考方案9】:

试试这个:

function algorithmToCalculateCombinations($n, $elems) 
        if ($n > 0) 
            $tmp_set = array();
            $res = algorithmToCalculateCombinations($n - 1, $elems);
            foreach ($res as $ce) 
                foreach ($elems as $e) 
                    array_push($tmp_set, $ce . $e);
                
            
            return $tmp_set;
         else 
            return array('');
        
    

$Elemen = array(range(0,9),range('a','z'));
$Length = 3;
$combinations = algorithmToCalculateCombinations($Length, $Elemen);

【讨论】:

以上是关于从多个值列表中查找所有不冲突的值组合的主要内容,如果未能解决你的问题,请参考以下文章

使用组合框中的值编辑从列表框中选择的记录中的字段

从包含 ACCESS 2013 中的多个表的表单中查找带有组合框的记录

在 vb.net 中查找列表值的所有组合(笛卡尔积)

使列表中所有可能的值组合大小不同

根据大型列表的多个条件查找所有组合

访问 VBA - 使用组合框(多值字段)时类型不匹配