我一直试图想出一种方法来生成多重集的所有不同大小的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的有效输出。)
关于如何解决这个问题的任何想法/技巧/现有算法?请注意,解决方案应该是高效的,即跟踪所有先前生成的解决方案,确定当前生成的解决方案是否是排列,如果是这样,则跳过它是不可行的,因为解决方案空间爆炸的时间越长,输入越多重复。
答案 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
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]
...