2013-09-21 25 views
5

我目前正在计算数据数组的唯一排列。虽然下面的代码正在工作,但并不像我想的那样高效。一旦我得到了6或8个项目,它变得非常慢,我开始遇到内存问题。高效计算集合中的唯一排列

下面是代码和一个解释

<?php 
function permuteUnique($items, $count = false, $perms = [], &$return = []) { 
    if ($count && count($return) == $count) return $return; 

    if (empty($items)) { 
     $duplicate = false; 

     foreach ($return as $a) { 
      if ($a === $perms) { 
       $duplicate = true; 
       break; 
      } 
     } 
     if (!$duplicate) $return[] = $perms; 
    } else { 
     for ($i = count($items) - 1; $i >= 0; --$i) { 
      $newitems = $items; 
      $newperms = $perms; 
      list($tmp) = array_splice($newitems, $i, 1); 
      array_unshift($newperms, $tmp); 
      permuteUnique($newitems, $count, $newperms, $return); 
     } 
     return $return; 
    } 
} 

function factorial($n) { 
    $f = 1; 
    for ($i = 2; $i <= $n; $i++) $f *= $i; 
    return $f; 
} 

鉴于我接收预期以下输出输入[1, 1, 2]

array (size=3) 
    0 => 
    array (size=3) 
     0 => int 1 
     1 => int 1 
     2 => int 2 
    1 => 
    array (size=3) 
     0 => int 1 
     1 => int 2 
     2 => int 1 
    2 => 
    array (size=3) 
     0 => int 2 
     1 => int 1 
     2 => int 1 

$count参数,所以我可以通过独特的排列数我期待该功能,一旦发现很多,它可以停止计算并返回数据。这被计算为项目总数的阶乘除以所有重复次数的阶乘的乘积。我不确定我是否说得对,所以让我举个例子。

给定了[1, 2, 2, 3, 4, 4, 4, 4]因为有8个项目总额,其中一人被复制两次独特排列的计数计算 8!/(2!4!) = 840,另一个是重复4次。

现在,如果我翻译,为PHP代码...

<?php 
$set = [1, 2, 2, 3, 4, 4, 4, 4]; 
$divisor = 1; 

foreach (array_count_values($set) as $v) { 
    $divisor *= factorial($v); 
} 

$count = factorial(count($set))/$divisor; 
$permutations = permuteUnique($set, $count); 

这是非常缓慢的。如果我在permuteUnique函数中抛出一个计数器,它会在找到840个唯一排列之前运行超过10万次。

我想找到一种方法来减少这种情况,并找到最短的路径,以独特的排列。我感谢您能给予的任何帮助或建议。

+0

查看C++的['std :: next_permutation'](http://en.cppreference.com/w/cpp/algorithm/next_permutation),找到或实现类似于PHP的东西。 – MvG

回答

5

所以我花了更多的时间思考这个,这就是我想出的。

<?php 
function permuteUnique($items, $perms = [], &$return = []) { 
    if (empty($items)) { 
     $return[] = $perms; 
    } else { 
     sort($items); 
     $prev = false; 
     for ($i = count($items) - 1; $i >= 0; --$i) { 
      $newitems = $items; 
      $tmp = array_splice($newitems, $i, 1)[0]; 
      if ($tmp != $prev) { 
       $prev = $tmp; 
       $newperms = $perms; 
       array_unshift($newperms, $tmp); 
       permuteUnique($newitems, $newperms, $return); 
      } 
     } 
     return $return; 
    } 
} 

$permutations = permuteUnique([1, 2, 2, 3, 4, 4, 4, 4]); 

上一页统计

Uniques: 840 
Calls to permuteUnique: 107,591 
Duplicates found: 38737 
Execution time (seconds): 4.898668050766 

新统计

Uniques: 840 
Calls to permuteUnique: 2647 
Duplicates found: 0 
Execution time (seconds): 0.0095300674438477 

因此,所有我真的是排序的数据集,跟踪上一个项目,而不是计算如果当前项目与前一项匹配,则进行排列。我也不再需要预先计算唯一身份的数量并遍历排列来检查重复项。这造成了一个不同的世界。

+1

在这一行* if($ tmp!= $ prev)*您应该使用*!= *的*!== * insetd。对于松散比较,当设定为0时,它会中断,例如, ** $ permutations = permuteUnique([0,1,1]); ** – f1ames

+1

你们用于分析和获取这些统计信息的数据 – rbz

2

我刚刚在wiki上尝试了“按字典顺序生成”的方式,并且它为您的“1,2,2,3,4,4,4,4”样本生成了相同的结果,所以我猜测它是正确的。下面是代码:

function &permuteUnique($items) { 
    sort($items); 
    $size = count($items); 
    $return = []; 
    while (true) { 
     $return[] = $items; 
     $invAt = $size - 2; 
     for (;;$invAt--) { 
      if ($invAt < 0) { 
       break 2; 
      } 
      if ($items[$invAt] < $items[$invAt + 1]) { 
       break; 
      } 
     } 
     $swap1Num = $items[$invAt]; 
     $inv2At = $size - 1; 
     while ($swap1Num >= $items[$inv2At]) { 
      $inv2At--; 
     } 
     $items[$invAt] = $items[$inv2At]; 
     $items[$inv2At] = $swap1Num; 
     $reverse1 = $invAt + 1; 
     $reverse2 = $size - 1; 
     while ($reverse1 < $reverse2) { 
      $temp = $items[$reverse1]; 
      $items[$reverse1] = $items[$reverse2]; 
      $items[$reverse2] = $temp; 
      $reverse1++; 
      $reverse2--; 
     } 
    } 
    return $return; 
} 

仿形为您的示例输入的时间:上述方法:2600,3000,3000,2400,2400,3000; 您的“调用permuteUnique:2647”方法:453425.6,454425.4,454625.8。在你的示例输入中,它大约快了500倍:)如果你正在逐一处理结果(我想你会),使用这种非递归方法,你可以处理一个生成的,然后生成下一个(而不是在处理之前生成全部并全部存储)。

+0

我不确定哪里获得了500倍的速度。它比我的测试快大约3-5倍,即使是更大的一套。仍然是一个非常好的答案。小心提供一个链接到您引用的wiki? – Rob

+0

@Rob:当然。它是http://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order我找到了一种方法来说它是正确的(以前我只是猜测)。这个500次的事情来自于剖析。 – daifei4321

0

试试这个修改后的迭代版本。它没有递归开销。

上找到: http://docstore.mik.ua/orelly/webprog/pcook/ch04_26.htm

ORIGINAL:

function pc_next_permutation($p, $size) { 
    // slide down the array looking for where we're smaller than the next guy 
    for ($i = $size - 1; $p[$i] >= $p[$i+1]; --$i) { } 

    // if this doesn't occur, we've finished our permutations 
    // the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1) 
    if ($i == -1) { return false; } 

    // slide down the array looking for a bigger number than what we found before 
    for ($j = $size; $p[$j] <= $p[$i]; --$j) { } 

    // swap them 
    $tmp = $p[$i]; $p[$i] = $p[$j]; $p[$j] = $tmp; 

    // now reverse the elements in between by swapping the ends 
    for (++$i, $j = $size; $i < $j; ++$i, --$j) { 
     $tmp = $p[$i]; $p[$i] = $p[$j]; $p[$j] = $tmp; 
    } 

    return $p; 
} 

$set = split(' ', 'she sells seashells'); // like array('she', 'sells', 'seashells') 
$size = count($set) - 1; 
$perm = range(0, $size); 
$j = 0; 

do { 
    foreach ($perm as $i) { $perms[$j][] = $set[$i]; } 
} while ($perm = pc_next_permutation($perm, $size) and ++$j); 

foreach ($perms as $p) { 
    print join(' ', $p) . "\n"; 
} 

下面是其修改为不同的排列一个想法,但我认为有更快的解决方案....

function pc_next_permutation($p, $size) { 
    for ($i = $size - 1; $p[$i] >= $p[$i+1]; --$i) { } 
    if ($i == -1) { return false; } 
    for ($j = $size; $p[$j] <= $p[$i]; --$j) { } 
    $tmp = $p[$i]; $p[$i] = $p[$j]; $p[$j] = $tmp; 
    for (++$i, $j = $size; $i < $j; ++$i, --$j) { 
     $tmp = $p[$i]; $p[$i] = $p[$j]; $p[$j] = $tmp; 
    } 

    return $p; 
} 

$uniqueMap=array(); 
$set = split(' ', '1 2 2 3 4 4 4 4'); 
$size = count($set) - 1; 
$perm = range(0, $size); 
$j=0; 

do { 
    $uniqueSetString=""; 
    foreach ($perm as $i) 
     $uniqueSetString .= "|".$set[$i]; 

    if (!isset($uniqueMap[$uniqueSetString])) 
    { 
     foreach ($perm as $i) 
      $perms[$j][] = $set[$i]; 

     $uniqueMap[$uniqueSetString]=1; 
    } 
} while ($perm = pc_next_permutation($perm, $size) and ++$j); 

foreach ($perms as $p) { 
    print join(' ', $p) . "\n"; 
} 
+1

未定义的偏移量:第3行的-1? :○ – hanshenrik

0

你需要的是factoriadic,它允许你生成第n个排列,而不需要所有的前面/后面g的。我使用PHP编写了代码,但是我没有使用ATM,对不起。

编辑Here you go,它应该让你开始。