从重叠的池中选择无序组合

时间:2018-08-14 05:47:53

标签: python combinations combinatorics

我有值池,我想通过从某些池中选择来生成每种可能的无序组合。

例如,我想从池0,池0和池1中进行选择:

>>> pools = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
>>> part = (0, 0, 1)
>>> list(product(*(pools[i] for i in part)))
[(1, 1, 2), (1, 1, 3), (1, 1, 4), (1, 2, 2), (1, 2, 3), (1, 2, 4), (1, 3, 2), (1, 3, 3), (1, 3, 4), (2, 1, 2), (2, 1, 3), (2, 1, 4), (2, 2, 2), (2, 2, 3), (2, 2, 4), (2, 3, 2), (2, 3, 3), (2, 3, 4), (3, 1, 2), (3, 1, 3), (3, 1, 4), (3, 2, 2), (3, 2, 3), (3, 2, 4), (3, 3, 2), (3, 3, 3), (3, 3, 4)]

这会通过从池0,池0和池1中进行选择来生成所有可能的组合。

但是顺序对我来说并不重要,因此许多组合实际上都是重复的。例如,由于我使用的是笛卡尔积,因此(1, 2, 4)(2, 1, 4)都会生成。

我想出了一种简单的方法来缓解此问题。对于从单个池中挑选的成员,我选择时不使用combinations_with_replacement进行排序。我计算要从每个池中抽奖的次数。代码如下:

cnt = Counter()
for ind in part: cnt[ind] += 1
blocks = [combinations_with_replacement(pools[i], cnt[i]) for i in cnt]
return [list(chain(*combo)) for combo in product(*blocks)]

如果我碰巧多次从同一个池中进行选择,这将减少重复项的排序。但是,所有池都有很多重叠,并且在合并的多个池上使用combinations_with_replacement会生成一些无效的组合。有没有更有效的方法来生成无序组合?

编辑:有关输入的额外信息:零件和池的数量很小(〜5和〜20),为简单起见,每个元素都是整数。我已经解决了实际问题,所以这只是出于学术目的。假设每个池中有成千上万个整数,但是有些池很小,只有几十个。因此,某种结合或相交似乎是可行的方法。

5 个答案:

答案 0 :(得分:8)

这是一个难题。我认为一般情况下,最好的选择是实现一个hash table,其中键是一个multiset,值是您的实际组合。这类似于@ErikWolf提到的内容,但是此方法避免了首先产生重复项,因此不需要过滤。当我们遇到multisets时,它还会返回正确的结果。

我现在正在嘲笑一种更快的解决方案,但可以保存以备后用。忍受我。

如评论中所述,一种可行的方法是合并所有池,并简单地生成此合并池的组合,然后选择池的数量。您将需要一种能够生成多集组合的工具,据我所知,该工具在python中可用。它在sympyfrom sympy.utilities.iterables import multiset_combinations中。这样做的问题是,我们仍然会产生重复的值,更糟糕的是,我们会产生类似的setproduct组合无法获得的结果。例如,如果我们要进行排序和合并OP中的所有池之类的操作并应用以下内容:

list(multiset_permutations([1,2,2,3,3,4,4,5]))

其中两个结果将是[1 2 2][4 4 5],它们都是无法从[[1, 2, 3], [2, 3, 4], [3, 4, 5]]获得的。

除特殊情况外,我看不到如何避免检查所有可能的产品。我希望我错了。

算法概述
主要思想是将向量乘积的组合映射为唯一组合,而不必过滤出重复项。 OP给出的示例(即(1, 2, 3)(1, 3, 2))应仅映射到一个值(因为顺序无关紧要,所以可以是两者之一)。我们注意到,两个向量是相同的集合。现在,我们还有类似的情况:

vec1 = (1, 2, 1)
vec2 = (2, 1, 1)
vec3 = (2, 2, 1)

我们需要vec1vec2来映射到相同的值,而vec3需要映射到它自己的值。这是集合的问题,因为所有这些都是等效的sets(对于集合,元素是唯一的,因此{a, b, b}{a, b}是等效的。)

这是multisets发挥作用的地方。对于多集,(2, 2, 1)(1, 2, 1)是不同的,但是(1, 2, 1)(2, 1, 1)是相同的。很好现在,我们有了一种生成唯一密钥的方法。

由于我不是python程序员,所以我将继续学习C++

如果我们尝试按原样实现上述所有内容,则会遇到一些问题。据我所知,您不能将std::multiset<int>作为std::unordered_map的关键部分。但是,我们可以使用常规的std::map。它的性能不如下面的哈希表(实际上是red-black tree),但仍然可以提供不错的性能。在这里:

void cartestionCombos(std::vector<std::vector<int> > v, bool verbose) {

    std::map<std::multiset<int>, std::vector<int> > cartCombs;

    unsigned long int len = v.size();
    unsigned long int myProd = 1;
    std::vector<unsigned long int> s(len);

    for (std::size_t j = 0; j < len; ++j) {
        myProd *= v[j].size();
        s[j] = v[j].size() - 1;
    }

    unsigned long int loopLim = myProd - 1;
    std::vector<std::vector<int> > res(myProd, std::vector<int>());
    std::vector<unsigned long int> myCounter(len, 0);
    std::vector<int> value(len, 0);
    std::multiset<int> key;

    for (std::size_t j = 0; j < loopLim; ++j) {
        key.clear();

        for (std::size_t k = 0; k < len; ++k) {
            value[k] = v[k][myCounter[k]];
            key.insert(value[k]);
        }

        cartCombs.insert({key, value});

        int test = 0;
        while (myCounter[test] == s[test]) {
            myCounter[test] = 0;
            ++test;
        }

        ++myCounter[test];
    }

    key.clear();
    // Get last possible combination
    for (std::size_t k = 0; k < len; ++k) {
        value[k] = v[k][myCounter[k]];
        key.insert(value[k]);
    }

    cartCombs.insert({key, value});

    if (verbose) {
        int count = 1;

        for (std::pair<std::multiset<int>, std::vector<int> > element : cartCombs) {
            std::string tempStr;

            for (std::size_t k = 0; k < len; ++k)
                tempStr += std::to_string(element.second[k]) + ' ';

            std::cout << count << " : " << tempStr << std::endl;
            ++count;
        }
    }
}

使用长度从4到8的8个向量的测试用例填充从1到15的随机整数,上述算法在我的计算机上运行约5秒钟。考虑到我们正在从我们的产品中获得近250万个总结果,这还不错,但是我们可以做得更好。但是如何?

std::unordered_map使用恒定时间构建的密钥可提供最佳性能。我们上面的密钥建立在对数时间(multiset, map and hash map complexity)中。所以问题是,我们如何克服这些障碍?

最佳表现

我们知道我们必须放弃std::multiset。我们需要某种具有commutative类型属性,同时又能提供独特结果的对象。

输入Fundamental Theorem of Arithmetic

它指出,每个数字都可以用质数的乘积唯一地表示(直到因子的顺序)。有时称为素数分解。

因此,现在,我们可以像以前一样简单地进行操作,但无需构造多集,而是将每个索引映射到素数并将结果相乘。这将为我们的密钥提供恒定的时间构造。这是一个示例,显示了此技术在我们之前创建的示例中的作用(下面的N.B. P是质数列表... (2, 3, 5, 7, 11, etc.)

                   Maps to                    Maps to            product
vec1 = (1, 2, 1)    -->>    P[1], P[2], P[1]   --->>   3, 5, 3    -->>    45
vec2 = (2, 1, 1)    -->>    P[2], P[1], P[1]   --->>   5, 3, 3    -->>    45
vec3 = (2, 2, 1)    -->>    P[2], P[2], P[1]   --->>   5, 5, 3    -->>    75

太棒了!! vec1vec2映射到相同的数字,而vec3映射到我们希望的其他值。

void cartestionCombosPrimes(std::vector<std::vector<int> > v, 
                        std::vector<int> primes,
                        bool verbose) {

    std::unordered_map<int64_t, std::vector<int> > cartCombs;

    unsigned long int len = v.size();
    unsigned long int myProd = 1;
    std::vector<unsigned long int> s(len);

    for (std::size_t j = 0; j < len; ++j) {
        myProd *= v[j].size();
        s[j] = v[j].size() - 1;
    }

    unsigned long int loopLim = myProd - 1;
    std::vector<std::vector<int> > res(myProd, std::vector<int>());
    std::vector<unsigned long int> myCounter(len, 0);
    std::vector<int> value(len, 0);
    int64_t key;

    for (std::size_t j = 0; j < loopLim; ++j) {
        key = 1;

        for (std::size_t k = 0; k < len; ++k) {
            value[k] = v[k][myCounter[k]];
            key *= primes[value[k]];
        }

        cartCombs.insert({key, value});

        int test = 0;
        while (myCounter[test] == s[test]) {
            myCounter[test] = 0;
            ++test;
        }

        ++myCounter[test];
    }

    key = 1;
    // Get last possible combination
    for (std::size_t k = 0; k < len; ++k) {
        value[k] = v[k][myCounter[k]];
        key *= primes[value[k]];
    }

    cartCombs.insert({key, value});
    std::cout << cartCombs.size() << std::endl;

    if (verbose) {
        int count = 1;

        for (std::pair<int, std::vector<int> > element : cartCombs) {
            std::string tempStr;

            for (std::size_t k = 0; k < len; ++k)
                tempStr += std::to_string(element.second[k]) + ' ';

            std::cout << count << " : " << tempStr << std::endl;
            ++count;
        }
    }
}

在上面的示例中,该示例将产生近250万个产品,上述算法在不到0.3秒的时间内返回了相同的结果。

后一种方法有两个警告。我们必须让素数生成一个先验,并且如果我们在笛卡尔积中有许多矢量,则密钥可能会超出int64_t的范围。由于存在许多可用于生成质数的资源(库,查找表等),第一个问题应该不难克服。我不太确定,但我读到对于python来说,后一个问题应该不是问题,因为整数具有任意精度(Python integer ranges)。

我们还必须处理以下事实:我们的源向量可能不是具有较小值的 nice 整数向量。在继续进行之前,可以通过对所有向量中的所有元素进行排名来解决此问题。例如,给定以下向量:

vec1 = (12345.65, 5, 5432.11111)
vec2 = (2222.22, 0.000005, 5)
vec3 = (5, 0.5, 0.8)

排名靠前,我们将获得:

rank1 = (6, 3, 5)
rank2 = (4, 0, 3)
rank3 = (3, 1, 2)

现在,可以使用这些值代替实际值来创建密钥。唯一会更改的代码部分是用于构建密钥的for循环(当然还有需要创建的rank对象):

for (std::size_t k = 0; k < len; ++k) {
    value[k] = v[k][myCounter[k]];
    key *= primes[rank[k][myCounter[k]]];
}

修改:
正如一些评论者所指出的那样,上述方法掩盖了必须生成所有产品的事实。我应该说是第一次。就个人而言,鉴于许多不同的演示,我看不出如何避免这种情况。

另外,万一有人好奇,这是我上面使用的测试用例:

[1 10 14  6],
[7  2  4  8  3 11 12],
[11  3 13  4 15  8  6  5],
[10  1  3  2  9  5  7],
[1  5 10  3  8 14],
[15  3  7 10  4  5  8  6],
[14  9 11 15],
[7  6 13 14 10 11  9  4]

它应该返回162295个唯一的组合。

答案 1 :(得分:7)

一种节省工作的方法可能是生成前k个选定池的重复数据消除组合,然后将其扩展到前k + 1个池的重复数据消除组合。这样一来,您就可以避免分别生成和拒绝从前两个池中选择2, 1而不是1, 2的所有长度为20的组合:

def combinations_from_pools(pools):
    # 1-element set whose one element is an empty tuple.
    # With no built-in hashable multiset type, sorted tuples are probably the most efficient
    # multiset representation.
    combos = {()}
    for pool in pools:
        combos = {tuple(sorted(combo + (elem,))) for combo in combos for elem in pool}
    return combos

尽管您使用的是输入大小,但是无论生成组合的效率如何,您都将永远无法处理所有组合。即使有20个相同的1000个元素池,也将有496432432432489450355564471512635900731810050组合(1019按星条形图选择20),或大约5e41。如果您征服了地球,并将全人类所有计算设备的全部处理能力都投入到了这项任务中,那么您仍然无法屈服。您需要找到一种更好的方法来解决基础任务。

答案 2 :(得分:5)

到目前为止已发布的答案(包括Tim Peters的lazy lexicographic one-at-a-time generation)在最坏情况下的空间复杂度与输出的大小成正比。我将概述一种方法,该方法将建设性地生成所有唯一的无序组合,而不会对内部生成的中间数据进行重复数据删除。我的算法按字典顺序生成组合。与较简单的算法相比,它具有计算开销。但是,它可以并行化(以便可以同时产生不同范围的最终输出)。

想法如下。

因此,我们有N个池{P 1 ,...,P N },必须从中提取组合。 我们可以轻松地确定最小的组合(相对于上述字典顺序)。设为(x 1 ,x 2 ...,x N-1 ,x N )(其中x 1 <= x 2 <= ... <= x N-1 <= x N ,并且每个x j 只是池{P i }中之一的最小元素。此最小组合后跟零个或多个组合,其中前缀x 1 ,x 2 ...,x N-1 是相同,最后一个位置的值序列不断增加。我们如何识别该序列?

让我们介绍以下定义:

  

给出一个组合前缀C =(x 1 ,x 2 ...,x K-1 ,x K )(其中K (如果是前缀C,则称池P i 相对于C ,称为 免费) )可以从其余的泳池中提取。

识别给定前缀的空闲池很容易地解决了在二部图中找到最大matchings的问题。具有挑战性的部分是有效地做到这一点(利用我们案件的具体内容)。但是我将其保存以备后用(这项工作正在进行中,有一天会变成Python程序)。

因此,对于第一个组合的前缀(x 1 ,x 2 ...,x N-1 ),我们可以标识所有空闲池{FP i }。它们中的任何一个都可以用来为最后一个位置选择一个元素。因此,感兴趣序列是{FP 1 U FP 2 U ...}中大于或等于x N-的元素的排序集合。 1

当最后一个位置用尽时,我们必须增加最后一个但只有一个位置,然后我们将重复查找最后一个位置的可能值的过程。毫不奇怪,枚举最后一个(以及其他任何一个)位置的值的过程是相同的-唯一的区别是组合前缀的长度,必须根据该长度来标识空闲池。

因此,以下递归算法可以完成工作:

  1. 以一个空的组合前缀C开头。这时所有池都是可用的。
  2. 如果C的长度等于N,则输出C并返回。
  3. 将空闲池合并到一个排序的列表S中,并从中删除所有小于C的最后一个元素的元素。
  4. 对于来自S do的每个值x
    • 新的组合前缀为C'=(C,x)
    • 在当前组合前缀增加了1的情况下,某些空闲池不再可用。识别它们,然后使用更新的空闲池列表和组合前缀C'进入步骤1。

答案 3 :(得分:3)

您可以实现可哈希列表,并使用python set()过滤所有重复项。 您的哈希函数只需要忽略列表中的顺序即可,这可以通过使用collections.Counter

来实现。
from collections import Counter

class HashableList(list):
    def __hash__(self):
        return hash(frozenset(Counter(self)))
    def __eq__(self, other):
        return hash(self) == hash(other)

x = HashableList([1,2,3])
y = HashableList([3,2,1])

print set([x,y])

这将返回:

set([[1, 2, 3]])

答案 4 :(得分:2)

这是我想出的:

class Combination:
    def __init__(self, combination):
        self.combination = tuple(sorted(combination))

    def __eq__(self, other):
        return self.combination == self.combination

    def __hash__(self):
        return self.combination.__hash__()

    def __repr__(self):
        return self.combination.__repr__()

    def __getitem__(self, i):
        return self.combination[i]

然后

pools = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
part = (0, 0, 1)
set(Combination(combin) for combin in product(*(pools[i] for i in part)))

输出:

{(1, 1, 2),
 (1, 1, 3),
 (1, 1, 4),
 (1, 2, 2),
 (1, 2, 3),
 (1, 2, 4),
 (1, 3, 3),
 (1, 3, 4),
 (2, 2, 2),
 (2, 2, 3),
 (2, 2, 4),
 (2, 3, 3),
 (2, 3, 4),
 (3, 3, 3),
 (3, 3, 4)}

不确定这是否是您真正要寻找的东西。