生成所有多集大小n分区的算法

时间:2016-05-27 12:33:36

标签: algorithm language-agnostic

我一直试图想出一种方法来生成多重集的所有不同大小的n分区,但到目前为止空手而归。首先让我展示一下我想要实现的目标。

我们假设我们的输入向量为uint32_t

std::vector<uint32_t> input = {1, 1, 2, 2}

让我们说我们要创建所有不同的2个大小的分区。其中只有两个,即:

[[1, 1], [2, 2]], [[1, 2], [1, 2]]

请注意,顺序并不重要,即以下所有内容都是重复的,不正确的解决方案。

  • 重复,因为排列组中的顺序无关紧要:

    [[2, 1], [1, 2]]
    
  • 重复,因为组的顺序无关紧要:

    [[2, 2], [1, 1]]
    

不是某种BTW的作业。我在编写工作代码时遇到了这个问题,但到目前为止,我想知道如何处理这个问题是出于个人兴趣。与工作相关的问题的参数足够小,产生几千个重复的解决方案并不重要。

当前解决方案(生成重复项)

为了说明我不是在没有试图提出解决方案的情况下提出要求,让我试着解释一下我当前的算法(当与多重集合一起使用时会产生重复的解决方案)。

它的工作原理如下:状态有一个bitset,每个分区块的n位设置为1。位集的长度为size(input) - n * index_block(),例如如果输入向量有8个元素且n = 2,则第一个分区块使用8位位组,2位设置为1,下一个分区块使用6位位组,2位设置为1,等等。 / p>

通过按顺序迭代每个位集并从中提取输入向量的元素,索引等于当前位集中的1位位置,从这些位集创建分区。

为了生成下一个分区,我以相反的顺序迭代位集。计算下一个bitset排列(使用Gosper&#hack的反向)。如果未设置当前位集中的第一位(即,未选择向量索引0),则将该位集复位到其开始状态。强制始终设置第一个位可防止在创建size-n set分区时生成重复项(上面显示的第二类重复项)。如果当前位集等于其起始值,则对前一个(更长)位集重复此步骤。

这对于集合来说效果很好(而且非常快)。但是,当与多集合一起使用时,它会生成重复的解决方案,因为它不知道两个元素在输入向量中出现多次。这是一些示例输出:

std::vector<uint32_t> input = {1, 2, 3, 4};
printAllSolutions(myCurrentAlgo(input, 2));
=> [[2, 1], [4, 3]], [[3, 1], [4, 2]], [[4, 1], [3, 2]]

std::vector<uint32_t> input = {1, 1, 2, 2};
printAllSolutions(myCurrentAlgo(input, 2));
=> [[1, 1], [2, 2]], [[2, 1], [2, 1]], [[2, 1], [2, 1]]

生成最后一个(重复)解决方案只是因为算法不知道输入中的重复,它会在两个示例中生成完全相同的内部状态(即要选择的索引)。

通缉解决方案

我想我现在已经很清楚我最终会想到什么了。仅仅为了完整起见,它看起来有点如下:

std::vector<uint32_t> multiset = {1, 1, 2, 2};
MagicClass myGenerator(multiset, 2);
do {
  std::vector<std::vector<uint32_t> > nextSolution = myGenerator.getCurrent();
  std::cout << nextSolution << std::endl;
} while (myGenerator.calcNext());
=> [[1, 1], [2, 2]]
   [[1, 2], [1, 2]]

即。代码的工作方式有点像std::next_permutation,告知已经生成了所有解决方案并且已经在#34;第一个&#34;解决方案(对于你想要使用的第一个定义,可能是按字典顺序排列,但不是必须)。

我找到的最接近的相关算法是来自Knuth的算法M,计算机程序设计的艺术,第4卷第1部分,第7.2.1.5节(第430页)。但是,这会生成所有可能的多集分区。书中还有一个关于如何修改Alg的练习(7.2.1.5.69,第778页的解决方案)。 M,以便仅生成最多r个分区的解决方案。但是,这仍然允许不同大小的分区(例如[[1, 2, 2], [1]]将是r = 2的有效输出。)

关于如何解决这个问题的任何想法/技巧/现有算法?请注意,解决方案应该是高效的,即跟踪所有先前生成的解决方案,确定当前生成的解决方案是否是排列,如果是这样,则跳过它是不可行的,因为解决方案空间爆炸的时间越长,输入越多重复。

3 个答案:

答案 0 :(得分:2)

逐个分发元素的递归算法可以基于一些简单的规则:

  • 首先对不同元素进行排序或计数;它们不必按任何特定顺序排列,您只想将相同的元素组合在一起。 (此步骤将简化以下某些步骤,但可以跳过。)
   {A,B,D,C,C,D,B,A,C} -> {A,A,B,B,D,D,C,C,C}  
  • 从一个空的解决方案开始,然后使用以下规则逐个插入元素:
   { , , } { , , } { , , }  
  • 在插入元素之前,找到重复的块,例如:
   {A, , } { , , } { , , }  
                    ^dup^

   {A, , } {A, , } {A, , }  
            ^dup^   ^dup^
  • 将元素插入每个具有可用空间的非重复块:
   partial solution: {A, , } {A, , } { , , }  
                              ^dup^

   insert element B: {A,B, } {A, , } { , , }  
                     {A, , } {A, , } {B, , }  
  • 如果已存在相同的元素,请不要将新元素放在其前面:
   partial solution:  {A, , } {B, , } { , , }  
   insert another B:  {A,B, } {B, , } { , , }  <- ILLEGAL  
                      {A, , } {B,B, } { , , }  <- OK
                      {A, , } {B, , } {B, , }  <- OK
  • 当插入其他N个相同元素的元素时,请确保在当前元素之后留下N个开放点:
   partial solution:  {A, , } {A, , } {B,B, }  
   insert first D:    {A,D, } {A, , } {B,B, }  <- OK  
                      {A, , } {A, , } {B,B,D}  <- ILLEGAL (NO SPACE FOR 2ND D)  
  • 最后一组相同的元素可以一次插入:
   partial solution:  {A,A, } {B,B,D} {D, , }  
   insert C,C,C:      {A,A,C} {B,B,D} {D,C,C}  

所以算法会是这样的:

// PREPARATION  
Sort or group input.              // {A,B,D,C,C,D,B,A,C} -> {A,A,B,B,D,D,C,C,C}  
Create empty partial solution.    // { , , } { , , } { , , }  
Start recursion with empty partial solution and index at start of input.  

// RECURSION  
Receive partial solution, index, group size and last-used block.  
If group size is zero:  
    Find group size of identical elements in input, starting at index.  
    Set last-used block to first block.  
Find empty places in partial solution, starting at last-used block.  
If index is at last group in input:  
    Fill empty spaces with elements of last group.
    Store complete solution.
    Return from recursion.
Mark duplicate blocks in partial solution.  
For each block in partial solution, starting at last-used block:  
    If current block is not a duplicate, and has empty places,  
    and the places left in current and later blocks is not less than the group size:
        Insert element into copy of partial solution.
        Recurse with copy, index + 1, group size - 1, current block.

我测试了这个算法的一个简单的JavaScript实现,它给出了正确的输出。

答案 1 :(得分:2)

这是一个有效的解决方案,它利用了HervéBrönnimann在N2639中提出的next_combination函数。评论应该使它非常明显。 “herve / combinatorics.hpp”文件包含herve命名空间内N2639中列出的代码。它是在C ++ 11/14中,转换为较旧的标准应该是非常简单的。

请注意,我只是快速测试了解决方案。此外,我在几分钟之前将其从基于类的实现中提取出来,因此一些额外的错误可能已经悄悄进入。快速的初始测试似乎证实它有效,但可能存在不会出现的极端情况。

#include <cstdint>
#include <iterator>

#include "herve/combinatorics.hpp"

template <typename BidirIter>
bool next_combination_partition (BidirIter const & startIt,
  BidirIter const & endIt, uint32_t const groupSize) {
  // Typedefs
  using tDiff = typename std::iterator_traits<BidirIter>::difference_type;

  // Skip the last partition, because is consists of the remaining elements.
  // Thus if there's 2 groups or less, the start should be at position 0.
  tDiff const totalLength = std::distance(startIt, endIt);
  uint32_t const numTotalGroups = std::max(static_cast<uint32_t>((totalLength - 1) / groupSize + 1), 2u);
  uint32_t curBegin = (numTotalGroups - 2) * groupSize;
  uint32_t const lastGroupBegin = curBegin - 1;
  uint32_t curMid = curBegin + groupSize;
  bool atStart = (totalLength != 0);

  // Iterate over combinations from back of list to front. If a combination ends
  // up at its starting value, update the previous one as well.
  for (; (curMid != 0) && (atStart);
    curMid = curBegin, curBegin -= groupSize) {
    // To prevent duplicates, first element of each combination partition needs
    // to be fixed. So move start iterator to the next element. This is not true
    // for the starting (2nd to last) group though.
    uint32_t const startIndex = std::min(curBegin + 1, lastGroupBegin + 1);
    auto const iterStart = std::next(startIt, startIndex);
    auto const iterMid = std::next(startIt, curMid);
    atStart = !herve::next_combination(iterStart, iterMid, endIt);
  }

  return !atStart;
}

编辑下面是我快速抛出的测试代码(“combopart.hpp”显然是包含上述功能的文件)。

#include "combopart.hpp"

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <iterator>
#include <vector>

int main (int argc, char* argv[]) {
  uint32_t const groupSize = 2;

  std::vector<uint32_t> v;
  v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  v = {0, 0, 0, 1, 1, 1, 2, 2, 2, 3};
  v = {1, 1, 2, 2};

  // Make sure contents are sorted
  std::sort(v.begin(), v.end());

  uint64_t count = 0;
  do {
    ++count;

    std::cout << "[ ";
    uint32_t elemCount = 0;
    for (auto it = v.begin(); it != v.end(); ++it) {
      std::cout << *it << " ";
      elemCount++;
      if ((elemCount % groupSize == 0) && (it != std::prev(v.end()))) {
        std::cout << "| ";
      }
    }
    std::cout << "]" << std::endl;
  } while (next_combination_partition(v.begin(), v.end(), groupSize));

  std::cout << std::endl << "# elements: " << v.size() << " - group size: " <<
    groupSize << " - # combination partitions: " << count << std::endl;

  return 0;
}

编辑2 改进的算法。使用条件移动(使用std::max)和将atStart布尔值设置为false替换早期退出分支。但未经测试,请注意。

编辑3 需要额外修改,以免“修复”第2个到最后一个分区中的第一个元素。附加代码应该编译为条件移动,因此不应该有与之关联的分支成本。

P.S。:我知道由@Howard Hinnant(可在https://howardhinnant.github.io/combinations.html获得)生成组合的代码比HervéBrönnimann的快得多。但是,该代码无法处理输入中的重复项(因为据我所见,它甚至从未取消引用迭代器),这是我的问题明确要求的。另一方面,如果您确定您的输入不会包含重复项,那么它肯定是您希望在上面的函数中使用的代码。

答案 2 :(得分:1)

这是我的铅笔和纸算法:

Describe the multiset in item quantities, e.g., {(1,2),(2,2)}

f(multiset,result):
  if the multiset is empty:
    return result
  otherwise:
    call f again with each unique distribution of one element added to result and 
    removed from the multiset state


Example:
{(1,2),(2,2),(3,2)} n = 2

11       -> 11 22    -> 11 22 33
            11 2  2  -> 11 23 23
1  1     -> 12 12    -> 12 12 33
            12 1  2  -> 12 13 23


Example:
{(1,2),(2,2),(3,2)} n = 3

11      -> 112 2   -> 112 233
           11  22  -> 113 223
1   1   -> 122 1   -> 122 133
           12  12  -> 123 123

让我们通过m69解决下面评论的问题,处理潜在的重复分布:

{A,B,B,C,C,D,D,D,D}

We've reached {A, , }{B, , }{B, , }, have 2 C's to distribute
and we'd like to avoid `ac  bc  b` generated along with `ac  b   bc`.

Because our generation in the level just above is ordered, the series of identical 
counts will be continuous. When a series of identical counts is encountered, make 
the assignment for the whole block of identical counts (rather than each one), 
and partition that contribution in descending parts; for example,

      | identical |
ac     b      b
ac     bc     b     // descending parts [1,0]

Example of longer block:

      |    identical block     |  descending parts
ac     bcccc  b      b      b    // [4,0,0,0] 
ac     bccc   bc     b      b    // [3,1,0,0]
ac     bcc    bcc    b      b    // [2,2,0,0]
...