获取可能的数组组合

Posted

技术标签:

【中文标题】获取可能的数组组合【英文标题】:Get possible array combinations 【发布时间】:2013-09-28 11:02:10 【问题描述】:

所以,

问题

从 SQL 我得到一个带有字符串的数组(平面数组)——让它成为现实

$rgData = ['foo', 'bar', 'baz', 'bee', 'feo'];

现在,我想得到这个数组的对和三元组的可能组合(以及,在常见情况下,4 个元素的组合等)。更具体地说:我的意思是 combinations 在数学意义上(没有重复),即那些计数等于

-so 对于上面的数组,对和三胞胎都是 10。

我的方法

我已经开始将 的可能值映射到可能的数组选定项。我当前的解决方案是指出一个元素是否被选为“1”,否则为“0”。对于上面的示例,它将是:

foo bar baz 蜜蜂 feo 0 0 1 1 1 -> [baz,蜜蜂,feo] 0 1 0 1 1 -> [酒吧,蜜蜂,feo] 0 1 1 0 1 -> [bar, baz, feo] 0 1 1 1 0 -> [酒吧,巴兹,蜜蜂] 1 0 0 1 1 -> [foo, 蜜蜂, feo] 1 0 1 0 1 -> [foo, baz, feo] 1 0 1 1 0 -> [foo, baz, 蜜蜂] 1 1 0 0 1 -> [foo, baz, feo] 1 1 0 1 0 -> [foo,bar,bee] 1 1 1 0 0 -> [foo, bar, baz]

我需要做的就是以某种方式生成所需的位集。这是我在 php 中的代码:

function nextAssoc($sAssoc)

   if(false !== ($iPos = strrpos($sAssoc, '01')))
   
      $sAssoc[$iPos]   = '1';
      $sAssoc[$iPos+1] = '0';
      return substr($sAssoc, 0, $iPos+2).
             str_repeat('0', substr_count(substr($sAssoc, $iPos+2), '0')).
             str_repeat('1', substr_count(substr($sAssoc, $iPos+2), '1'));
   
   return false;


function getAssoc(array $rgData, $iCount=2)

   if(count($rgData)<$iCount)
   
      return null;
   
   $sAssoc   = str_repeat('0', count($rgData)-$iCount).str_repeat('1', $iCount);
   $rgResult = [];
   do
   
      $rgResult[]=array_intersect_key($rgData, array_filter(str_split($sAssoc)));
   
   while($sAssoc=nextAssoc($sAssoc));
   return $rgResult;

-我选择将我的位存储为普通字符串。我产生下一个关联的算法是:

    尝试查找“01”。如果没有找到,那么它是 11..100..0 的情况(所以它是最大的,找不到更多的)。如果找到,请转到第二步 转到字符串中“01”的最右边位置。将其切换到“10”,然后将所有比找到的“01”位置更右侧的零移动到左侧。 比如01110:“01”最右边的位置是0,所以我们先把这个“01”换成“10”。字符串现在仍然是10110。现在,转到右边部分(它没有10 部分,所以它从 0+2=2-nd 符号开始),并将所有零向左移动,即 110 将是 011。因此,我们将 10+011=10111 作为 01110 的下一个关联。

我发现了类似的问题here - 但是 OP 想要有重复的组合,而我希望它们没有重复。

问题

我的问题是关于两点:

对于我的解决方案,是否有另一种方法可以更有效地生成下一个位集? 可能有更简单的解决方案吗?这似乎是标准问题。

【问题讨论】:

Algorithm to return all combinations of k elements from n的可能重复 哇,这看起来很有趣,谢谢@ElKamina 我认为我的回答很好,在干净的 PHP 代码和速度之间取得了很好的平衡。你只是忽略了它吗? 【参考方案1】:

很抱歉没有提供 PHP 解决方案,因为我已经很长时间没有使用 PHP 编程了,但让我向您展示一个快速的 Scala 解决方案。也许它会激励你:

val array = Vector("foo", "bar", "baz", "bee", "feo")
for (i <- 0 until array.size; 
     j <- i + 1 until array.size; 
     k <- j + 1 until array.size)      
    yield (array(i), array(j), array(k))

结果:

Vector((foo,bar,baz), (foo,bar,bee), (foo,bar,feo), (foo,baz,bee), (foo,baz,feo), (foo,bee,feo), (bar,baz,bee), (bar,baz,feo), (bar,bee,feo), (baz,bee,feo))

生成k-组合的通用代码:

def combinations(array: Vector[String], k: Int, start: Int = 0): Iterable[List[String]] =  
  if (k == 1 || start == array.length) 
    for (i <- start until array.length) yield List(array(i))
  else 
    for (i <- start until array.length; c <- combinations(array, k - 1, i + 1)) yield array(i) :: c 

结果:

scala> combinations(Vector("a", "b", "c", "d", "e"), 1)
res8: Iterable[List[String]] = Vector(List(a), List(b), List(c), List(d), List(e))

scala> combinations(Vector("a", "b", "c", "d", "e"), 2)
res9: Iterable[List[String]] = Vector(List(a, b), List(a, c), List(a, d), List(a, e), List(b, c), List(b, d), List(b, e), List(c, d), List(c, e), List(d, e))

scala> combinations(Vector("a", "b", "c", "d", "e"), 3)
res10: Iterable[List[String]] = Vector(List(a, b, c), List(a, b, d), List(a, b, e), List(a, c, d), List(a, c, e), List(a, d, e), List(b, c, d), List(b, c, e), List(b, d, e), List(c, d, e))

scala> combinations(Vector("a", "b", "c", "d", "e"), 4)
res11: Iterable[List[String]] = Vector(List(a, b, c, d), List(a, b, c, e), List(a, b, d, e), List(a, c, d, e), List(b, c, d, e))

scala> combinations(Vector("a", "b", "c", "d", "e"), 5)
res12: Iterable[List[String]] = Vector(List(a, b, c, d, e))

当然,真正的 Scala 代码在接受的元素类型和集合类型方面应该更加通用,但我只是想展示基本思想,而不是最漂亮的 Scala 代码。

【讨论】:

如果我想获得 4 个元素的组合怎么办?还是5?如果不修改您的代码,我将无法做到这一点。常见问题是从N获取K 好吧,我误解了你只需要对和三对。获得任何 k 组合并不难,但您需要使用递归。 你能推荐你的变种吗? (带递归) 谢谢,@Piotr - 我知道这是一个很好的建议。现在,我在问题上方的链接中找到了答案【参考方案2】:

这是一个递归解决方案:

function subcombi($arr, $arr_size, $count)

   $combi_arr = array();
   if ($count > 1) 
      for ($i = $count - 1; $i < $arr_size; $i++) 
         $highest_index_elem_arr = array($i => $arr[$i]);
         foreach (subcombi($arr, $i, $count - 1) as $subcombi_arr) 
            $combi_arr[] = $subcombi_arr + $highest_index_elem_arr;
         
      
    else 
      for ($i = $count - 1; $i < $arr_size; $i++) 
         $combi_arr[] = array($i => $arr[$i]);
      
   
   return $combi_arr;


function combinations($arr, $count)

   if ( !(0 <= $count && $count <= count($arr))) 
      return false;
   
   return $count ? subcombi($arr, count($arr), $count) : array();
    

$input_arr = array('foo', 'bar', 'baz', 'bee', 'feo');
$combi_arr = combinations($input_arr, 3);
var_export($combi_arr); echo ";\n";

OUTPUT:

array (
  0 => 
  array (
    0 => 'foo',
    1 => 'bar',
    2 => 'baz',
  ),
  1 => 
  array (
    0 => 'foo',
    1 => 'bar',
    3 => 'bee',
  ),
  2 => 
  array (
    0 => 'foo',
    2 => 'baz',
    3 => 'bee',
  ),
  3 => 
  array (
    1 => 'bar',
    2 => 'baz',
    3 => 'bee',
  ),
  4 => 
  array (
    0 => 'foo',
    1 => 'bar',
    4 => 'feo',
  ),
  5 => 
  array (
    0 => 'foo',
    2 => 'baz',
    4 => 'feo',
  ),
  6 => 
  array (
    1 => 'bar',
    2 => 'baz',
    4 => 'feo',
  ),
  7 => 
  array (
    0 => 'foo',
    3 => 'bee',
    4 => 'feo',
  ),
  8 => 
  array (
    1 => 'bar',
    3 => 'bee',
    4 => 'feo',
  ),
  9 => 
  array (
    2 => 'baz',
    3 => 'bee',
    4 => 'feo',
  ),
);

递归是基于这样一个事实,即要从n ($arr_size) 中获取k ($count) 元素的所有组合,对于从零开始的最高索引@ 的所有可能选择,您必须987654327@,从剩余的i元素中找出所有k-1元素的“子组合”,索引低于i

为了利用 PHP 的“延迟复制”机制,将数组传递给递归调用时,该数组不是 array_sliced。这样就不会发生真正的复制,因为数组没有被修改。

保留数组索引非常适合调试目的,但这不是必需的。令人惊讶的是,简单地删除 $i =&gt; 部分并将数组 + 替换为 array_merge 会导致相当大的减速。要获得比原始版本稍快的速度,您必须这样做:

function subcombi($arr, $arr_size, $count)

   $combi_arr = array();
   if ($count > 1) 
      for ($i = $count - 1; $i < $arr_size; $i++) 
         $highest_index_elem = $arr[$i];
         foreach (subcombi($arr, $i, $count - 1) as $subcombi_arr) 
            $subcombi_arr[] = $highest_index_elem;
            $combi_arr[] = $subcombi_arr;
         
      
    else 
      for ($i = $count - 1; $i < $arr_size; $i++) 
         $combi_arr[] = array($arr[$i]);
      
   
   return $combi_arr;


关于问题的第一部分,您应该避免多次计算相同的数量,并且应该尽量减少函数调用。例如,像这样:
function nextAssoc($sAssoc)

   if (false !== ($iPos = strrpos($sAssoc, '01')))
   
      $sAssoc[$iPos]   = '1';
      $sAssoc[$iPos+1] = '0';
      $tailPos = $iPos+2;
      $n0 = substr_count($sAssoc, '0', $tailPos);
      $n1 = strlen($sAssoc) - $tailPos - $n0;
      return substr($sAssoc, 0, $tailPos).str_repeat('0', $n0)
                                         .str_repeat('1', $n1);
   
   return false;

如果不彻底改变代码,很难对代码进行更深入的更改。不过还不错,因为在我的测试中,它的速度大约是我的递归解决方案的一半(即,时间大约是两倍)

【讨论】:

嗨,沃尔特,现在我看了一下。由于我的原始解决方案是“最小建设性”解决方案(即我正在构建精确的二进制投影并且没有过多) - 它只能在语言/表达级别上进行改进(正如我在您的解决方案中看到的那样)。所以两种解决方案都有相同的大 O 估计,但是,你可以有更好的前导常数。谢谢你。另外 - 我记得,PHP 默认情况下仅通过引用传递对象,因此在函数中接受数组作为引用可能会很好,以防止复制到本地函数堆栈。 @AlmaDoMundo:我担心大 O 再好不过了。关于数组,正如我在回答中解释的那样,它们是被复制的,但是该副本是“惰性副本”(请参阅​​en.wikipedia.org/wiki/Object_copy#Lazy_copy),因此只要它们没有被写入就不需要通过引用传递它们,实际上最好按值传递。 我知道 - 由于我们都有最小的建设性解决方案,算法本身无法改进。我知道“惰性复制”(但我认为将其命名为“写入时复制”更正确——就像在 PHP 中一样)。我不确定是否要处理到本地堆栈的通道 - 如果它也没有被复制,那么你是对的 - 不需要通过引用传递。 @AlmaDoMundo:是的,堆栈上的副本以及对返回值的副本的行为就像任何赋值副本一样。无论如何,我测量了有和没有&amp; 的时间,我发现没有可测量的差异。 是的 - 我也是(即发现与测试没有区别)【参考方案3】:

我刚刚尝试在不使用 go 语言的递归的情况下以最小的时间复杂度解决这个问题。

我见过一些解决方案,但都是使用递归函数。避免递归解决堆栈大小超出错误。

package main

import "fmt"

func main() 
    // Arguments
    arr := []string"foo", "bar", "baz", "bee", "feo", "boo", "bak"
    combinations := make([][]string, 0)
    k := 4
    n := len(arr)

    // Execution starts from here
    if k > n 
        panic("invalid requirement")
    

    pos := make([]int, k) // this variable is used to plot the unique combination of elements

    // initialize an array with first ever plotting possitions
    i := 0
    c := k
    for c > 0 
        c--
        pos[i] = c
        i++
    
    combinations = append(combinations, getCombination(arr, pos, k))

    // Let's begin the work
    x := 0
    ctr := 1 // counter is use to calculate total iterations
    for pos[x] < n-(x+1) 
        ctr++
        pos[x]++

        combinations = append(combinations, getCombination(arr, pos, k))

        if pos[x] == n-(x+1) && x+1 < k 
            x++
            i := x
            s := pos[x] + 1
            for i > 0 
                i--
                s++
                pos[i] = s
            

            // continue to next index
            continue
        

        x = 0

    

    fmt.Println("total # iterations: --> ", ctr)

    fmt.Println(combinations, "\ntotal # combinations: ", len(combinations))



func getCombination(arr []string, pos []int, k int) []string 
    combination := make([]string, k)
    for i, j := k-1, 0; i >= 0; i, j = i-1, j+1 
        combination[j] = arr[pos[i]]
    
    return combination

工作示例在这里https://play.golang.org/p/D6I5aq8685-

【讨论】:

以上是关于获取可能的数组组合的主要内容,如果未能解决你的问题,请参考以下文章

使用 JavaScript 将两个数组的所有可能组合作为数组数组获取

如何从多个数组中获取所有组合?

获取数组的所有组合(如果我将它们一一删除)[重复]

Ruby中数组哈希的所有可能组合

JavaScript,从几个数组中获取所有唯一组合[重复]

获取字符串的所有组合