重复组合计数

Posted

技术标签:

【中文标题】重复组合计数【英文标题】:Count of combinations with repetitions 【发布时间】:2018-11-29 15:25:19 【问题描述】:

我有一种非常低效的方法来计算大小为 N 的数组中 N/2 项的组合。我所做的是对数组进行排序,然后循环遍历数组的排列,创建具有一半的多重集元素并将其插入到集合中。最后我得到了集合的计数。

long GetCombinations(std::vector<double> nums) 
    long combinations = 0;
    std::sort(nums.begin(), nums.end());
    std::set<std::multiset<double>> super_set;

    do 
        std::multiset<double> multi_set;

        for (unsigned int i = 0; i < nums.size() / 2; ++i)
            multi_set.insert(nums[i]);

        auto el = (super_set.insert(multi_set));

        if (el.second)
            ++combinations;

     while (std::next_permutation(nums.begin(), nums.end()));

    return combinations;

代码有效,但效率很低。对于给定的数组[0.5, 0.5, 1, 1],大小为 2 的有 3 种组合:

0.5, 0.5 1, 1 1, 0.5

是否有不同的算法或方法可以提高此代码的速度?

【问题讨论】:

如果代码有效,而您只想提高性能,codereview.stackexchange.com 更适合此类问题。 可能有 4 种组合 0.5 , 1 , 1, 0.5 , 0.5 , 0.5 , 1, 1 ? 有很好的公式来计算组合的数量。正确的解决方案是使用正确的数学。见en.wikipedia.org/wiki/Combination @AndrewKashpur 生成组合时顺序无关紧要。 1, 0.50.5, 1 的组合。如果顺序很重要,那就是排列。 为什么不直接计算二项式系数的值? 【参考方案1】:

计数组合

一般来说,确定特定集合的组合数量是非常简单的。但是,将其扩展到每个元素重复特定次数的多重集要困难得多,并且没有很好的文档记录。 @WorldSEnder 链接到一个数学/stackexchange 答案,该答案有一个 comment,并链接到 Frank Ruskey 的这篇名为 Combinatorial Generation 的精彩组合文章的链接。如果您转到第 71 页,则有一个部分更严格地处理了这个主题。

基本定义

    Set - 不同对象的集合。 - 例如。 a, ba, a, b 相同,并且都具有基数 2 Multiset - 类似于集合,但允许重复条目。 - 例如。 a, ba, a, b 是不同的多重集,基数分别为 2 和 3 二项式系数 - 给出 n 元素集的 k 元素子集的数量。 Multiset Coefficient/Number - 基数 k 的多重集的数量,其中元素取自有限集。

误解

人们相信有一个简单的公式可以快速计算长度为 k 的多重集的组合数量,其中每个元素重复特定次数(请参阅高度赞成的 cmets更多)。下面,我们将逐一研究众所周知的方法。

让我们从二项式系数的一般应用开始。我们立即看到这将失败,因为它严格意味着计算 set 的组合数,其中不允许重复条目。在我们的例子中,允许重复。

在***页面上进一步阅读,有一个部分称为Number of combinations with repetition。这看起来很有希望,因为我们确实有 一些 复制。我们还看到了修改后的二项式系数,这似乎更有希望。仔细观察会发现这也将失败,因为这严格适用于每个元素重复最多 k 次的多重集。

最后,我们试试multiset coefficient。列出的示例之一看起来与我们想要完成的非常相似。

“首先,考虑表示 a, a, a, a, a, a, b, b, c, c, c, d, d, d, d, d, d, d (6 as, 2 bs, 3 cs, 7 ds) 这种形式:"

这看起来是我们试图推导的一个很好的候选。但是,您会看到他们继续推导出可以从一组 4 个不同元素中构造多组基数 18 的方法的数量。这相当于长度为 4 的 18 个 integer compositions 的数量。例如

18 + 0 + 0 + 0
17 + 1 + 0 + 0
16 + 2 + 0 + 0
       .
       .
       .
5 +  4 + 6 + 3
4 +  5 + 6 + 3
3 +  6 + 6 + 3
       .
       .
       .
0 +  1 + 0 + 17
0 +  0 + 1 + 17
0 +  0 + 0 + 18

如您所见,顺序很重要,显然不适用于我们的情况。

最后提到的两种方法源自著名的Stars and Bars 方法,用于解决简单的计数问题。据我所知,这种方法不容易扩展到我们的案例。

一种工作算法

unsigned long int getCombinationCount(std::vector<double> nums) 

    unsigned long int n = nums.size();
    unsigned long int n2 = n / 2;
    unsigned long int numUnique = 1;
    unsigned long int numCombinations;

    std::sort(nums.begin(), nums.end());
    std::vector<int> numReps;

    double testVal = nums[0];
    numReps.push_back(1);

    for (std::size_t i = 1; i < n; ++i) 
        if (nums[i] != testVal) 
            numReps.push_back(1);
            testVal = nums[i];
            ++numUnique;
         else 
            ++numReps[numUnique - 1];
        
    

    int myMax, r = n2 + 1;
    std::vector<double> triangleVec(r);
    std::vector<double> temp(r);
    double tempSum;

    myMax = r;
    if (myMax > numReps[0] + 1)
        myMax = numReps[0] + 1;

    for (int i = 0; i < myMax; ++i)
        triangleVec[i] = 1;

    temp = triangleVec;

    for (std::size_t k = 1; k < numUnique; ++k) 
        for (int i = n2; i > 0; --i) 
            myMax = i - numReps[k];
            if (myMax < 0)
                myMax = 0;

            tempSum = 0;
            for (int j = myMax; j <= i; ++j)
                tempSum += triangleVec[j];

            temp[i] = tempSum;
        
        triangleVec = temp;
    

    numCombinations = (unsigned long int) triangleVec[n2];

    return numCombinations;

使用修正帕斯卡三角形的解释

传统的Pascal's Triangle(PT 从这里开始)中的条目表示二项式系数,其中三角形的行是您集合中的元素数,列是您希望生成的组合的长度。三角形的构造是我们如何解决手头问题的关键。

如果您注意到对于传统的 PT,要获取特定条目,请说 (i, j) 其中 i 是行,而 j em> 是列,您必须添加条目 (i - 1, j - 1)(i - 1, j)。这是一个插图。

                  1
                1   1
              1   2   1            N.B. The first 10 is in the 5th row and 3rd column
            1   3   3   1               and is obtained by adding the entries from the
          1   4   6   4   1             4th row and 2nd/3rd.
        1   5   10  10  5   1
      1   6   15  20  15  6   1

我们可以将其扩展到一般的多重集,其中每个元素都重复特定的次数。让我们考虑几个例子。

示例 1:v1 = 1, 2, 2v2 = 1, 2, 2, 3, 3, 3v3 = 1,2,2,3,3,3,4,4,4,4

下面是v1 choose 1 - 3v2 choose 1 - 6 的所有可能组合。

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

     [,1] [,2]               [,1] [,2]
[1,]    1    2          [1,]    1    2
[2,]    2    2          [2,]    1    3
                        [3,]    2    2
                        [4,]    2    3
                        [5,]    3    3

     [,1] [,2] [,3]          [,1] [,2] [,3]
[1,]    1    2    2     [1,]    1    2    2
                        [2,]    1    2    3
                        [3,]    1    3    3
                        [4,]    2    2    3
                        [5,]    2    3    3
                        [6,]    3    3    3

                             [,1] [,2] [,3] [,4]
                        [1,]    1    2    2    3
                        [2,]    1    2    3    3
                        [3,]    1    3    3    3
                        [4,]    2    2    3    3
                        [5,]    2    3    3    3

                             [,1] [,2] [,3] [,4] [,5]
                        [1,]    1    2    2    3    3
                        [2,]    1    2    3    3    3
                        [3,]    2    2    3    3    3

                             [,1] [,2] [,3] [,4] [,5] [,6]
                        [1,]    1    2    2    3    3    3

让我们写下 v1v2 的所有 k 组合的数量。

2  2  1
3  5  6  5  3  1

我将为您提供v3 的所有 k 个组合的数量(我将留给读者列举它们)。

4  9 15 20 22 20 15  9  4  1

我们以一种特殊的方式结合上面的结果,并注意到事情开始看起来非常熟悉。

         2  2  1
     3   5   6   5  3  1
4  9  15  20  22  20  15  9  4  1

我们添加几个作为占位符来完成这个修改后的 PT

                1   1
            1   2   2   1
      1   3   5   6   5   3   1
1  4  9  15  20  22  20  15   9  4  1

这是什么意思?有点清楚的是,每一行中的数字是前一行中的数字的组合。但是怎么做呢?......

我们让每个元素的频率指导我们。

例如,要获得代表v2 choose 1 - 6的组合数的第三行(忽略第一个1),我们查看第2行。由于第3个元素的频率为3,我们将4个元素(3 + 1 .. 就像用于查找具有不同元素的集合组合数量的二项式系数一样,我们将 2 个条目添加到一起或 1 + 1) 在上面的行中,列小于或等于我们找到的列。所以我们有:

if the column index is non-positive or greater than the 
number of columns in the previous row, the value is 0

    v2 choose 3
(3, 2) =  (2, 2 - 3) + (2, 2 - 2) + (2, 2 - 1) + (2, 2 - 0)
       =       0     +      0     +      1     +    2 
       =   3

v2 choose 4           
(3, 3) =  (2, 3 - 3) + (2, 3 - 2) + (2, 3 - 1) + (2, 3 - 0)
       =       0     +      1     +      2     +    2 
       =   5           

v2 choose 5 
(3, 4) =  (2, 4 - 3) + (2, 4 - 2) + (2, 4 - 1) + (2, 4 - 0)
       =       1     +      2     +      2     +    1 
       =   6

v2 choose 6                                   outside of range
(3, 5) =  (2, 5 - 3) + (2, 5 - 2) + (2, 5 - 1) + (2, 5 - 0)
       =       2     +      2     +      1     +    0 
       =   5

       etc.

继续这个逻辑,让我们看看我们是否可以获得v3k-组合的数量。由于第 4 个元素的频率是 4,我们需要将 5 个条目加在一起。

v3 choose 3
(4, 2) =  (3, 2 - 4) + (3, 2 - 3) + (3, 2 - 2) + (3, 2 - 1) + (3, 2 - 0)
       =       0     +      0     +     0      +      1     +     3 
       =   4

v3 choose 4 
(4, 3) =  (3, 3 - 4) + (3, 3 - 3) + (3, 3 - 2) + (3, 3 - 1) + (3, 3 - 0)
       =       0     +      0     +      1     +    3       +     5
       =   9           

v3 choose 5  
(4, 4) =  (3, 4 - 4) + (3, 4 - 3) + (3, 4 - 2) + (3, 4 - 1) + (3, 4 - 0)
       =       0     +     1      +      3     +     5      +     6
       =   15

v3 choose 6
(4, 5) =  (3, 5 - 4) + (3, 5 - 3) + (3, 5 - 2) + (3, 5 - 1) + (3, 5 - 0)
       =       1     +     3      +      5     +       6    +    5
       =   20

       etc.

事实上,我们确实得到了正确数量的 k-v3 组合。

示例 2:z1 = 1,1,1,2z2 = 1,1,1,1,2,3,3,3,3,3z3 = 1,1,1,1,2,3,3,3,3,3,4,4

您会注意到,我们正在构建这些向量,使得每个连续的向量都包含先前的向量。我们这样做是为了能够正确构建修改后的 PT。这类似于传统的 PT,在每个连续行中,我们只是将一个数字添加到前一行。这些向量的修改后的 PT 是:

                1   1   1  1
             1   2   2   2   1
      1  3  5  7   8   8   7   5   3  1
  1  4   9  15  20  23   23  20  15  9  4  1

让我们构造z2 choose 6z3 choose 9 看看我们是否正确:

 z2 choose 6
      [,1] [,2] [,3] [,4] [,5] [,6]
 [1,]    1    1    1    2    3    3
 [2,]    1    1    1    3    3    3      This shows that we produce 7 combs
 [3,]    1    1    2    3    3    3      just as predicted by our modified
 [4,]    1    1    3    3    3    3      PT (i.e. entry (3, 6 + 1) = 7)
 [5,]    1    2    3    3    3    3
 [6,]    1    3    3    3    3    3
 [7,]    2    3    3    3    3    3


 z3 choose 9
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
[1,]    1    1    1    2    3    3    3    3    3
[2,]    1    1    1    2    3    3    3    3    4
[3,]    1    1    1    2    3    3    3    4    4  This shows that we produce 9 
[4,]    1    1    1    3    3    3    3    3    4  combs just as predicted by 
[5,]    1    1    1    3    3    3    3    4    4  our modified PT (i.e. entry
[6,]    1    1    2    3    3    3    3    3    4  (4, 9 + 1) = 9)
[7,]    1    1    2    3    3    3    3    4    4
[8,]    1    1    3    3    3    3    3    4    4
[9,]    1    2    3    3    3    3    3    4    4

作为一个快速说明,第一行占位符类似于传统 PT 的第二行(即1 1)。粗略地说(见边缘情况的代码),如果第一个元素的频率为 m,则修改后的 PT 的第一行将包含 m + 1 个。

没有通用公式的原因(例如类似于二项式系数的东西)

从上面的两个例子可以看出,修改后的 PT 是基于特定的多集,因此不能一概而论。即使您考虑由相同不同元素组成的某些基数的多重集,修改后的 PT 也会有所不同。例如,多重集a = 1, 2, 2, 3, 3, 3b = 1, 1, 2, 2, 3, 3 分别生成以下修改后的 PT:

     1 1
   1 2 2 1
1 3 5 6 5 3 1

    1 1 1
  1 2 3 2 1
1 3 6 7 6 3 1

注意a choose 2 = 5b choose 2 = 6

基准测试:

这里是ideone 的链接,展示了新算法的加速。对于向量4, 2, 6, 4, 9, 8, 2, 4, 1, 1, 6, 9,原始的计时是2285718 时钟滴答,而上面的算法在8 时钟滴答中完成,总速度提高了2285728 / 8 = 285714.75...超过十万倍。它们都返回相同数量的组合(即 122)。大多数速度提升来自避免显式生成任何组合(或 OP 代码所做的排列)。

【讨论】:

@JosephWood 谢谢。这肯定会更快,但解释会有所帮助。 我很难清楚地解释这个算法。如果有人认为他们可以增加清晰度,我很乐意将其转换为社区 wiki。 我能理解矩阵的理论。但是,我仍然对代码有所了解。 'myMax' 是做什么用的?你不是在为'triangleVec'中的组合总和构建一个数组吗? @WorldSEnder,非常感谢您提供的链接.. cmets 中有一篇很棒的文章来回答。

以上是关于重复组合计数的主要内容,如果未能解决你的问题,请参考以下文章

SQL 语句取合计数

用SQL实现统计报表中的“小计”和“合计”

如何合计计算字段的每个枚举的总数?

02.规划过程组表格-WBS词典

sql 分组小计与合计语法

delphi cxgrid 获取合计行数值