有效地计算集合中的唯一排列

时间:2013-09-21 18:02:29

标签: php math permutation factorial

我目前正在计算数据数组的唯一排列。虽然以下代码正在运行,但它并不像我想的那样高效。一旦我超过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! / (2!4!) = 840因为共有8个项目,其中一个重复两次,另一个重复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个唯一排列之前运行超过100k次。

我想找到一种方法来减少这种情况并找到最短路径来获得独特的排列。我感谢您提供的任何帮助或建议。

4 个答案:

答案 0 :(得分: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 :(得分:2)

我刚试过wiki上的“按字典顺序生成”的方式,它会为你的“1,2,2,3,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倍:)如果你逐个处理结果(我想你会),使用这个非递归方法,你可以处理一个生成然后生成下一个(而不是在处理之前生成所有并存储所有内容。)

答案 2 :(得分: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";
}

答案 3 :(得分:0)

你需要的是factoriadic,它允许你生成第n个排列,而不需要所有之前/之后的排列。我用PHP编写了它,但我没有ATM,抱歉。

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

相关问题