如何(廉价)计算 n 个可能元素的所有可能的长度-r 组合

Posted

技术标签:

【中文标题】如何(廉价)计算 n 个可能元素的所有可能的长度-r 组合【英文标题】:How to (cheaply) calculate all possible length-r combinations of n possible elements 【发布时间】:2015-02-03 04:24:02 【问题描述】:

在不诉诸蛮力技术或任何需要 STL 的情况下,计算 n 个可能元素的所有可能长度-r 组合的最快方法是什么?

在为我的数据结构课程的最后一个项目研究 Apriori 算法时,我开发了一个有趣的解决方案,该解决方案使用位移和递归,我将在下面为感兴趣的人分享答案。但是,这是实现这一目标的最快方法吗(不使用任何通用库)?

我问的更多是出于好奇,因为我目前拥有的算法对我的目的来说工作得很好。

【问题讨论】:

"Gosper's hack" 通过迭代直到 n 且按顺序设置了 k 位的数字来做到这一点。这……很聪明。 @tmyklebu 看起来我在不知道这个黑客有它的名字的情况下发布了它作为答案。我在 JVM 源代码中发现了这个,没有任何 cmets。 与bit-hack-to-generate-all-integers-with-a-given-number-of-1s相关 其实,对于Apriori,你只需要生成一个长度为n的项集的n-1个子集。这种情况下的方法比任意长度的所有子集的一般情况更简单。 @Phil 这仅在修剪阶段消除非频繁集时才成立。在为算法的下一轮生成新的候选列表时,您需要使所有可能的长度为 n+1 的集合来自修剪后剩余的集合中的所有项目(其中 n 是来自上一轮的数据集的大小)。对于大型数据集,这可能会导致许多组合。实际上,我必须编写一个包含布尔数组的类,并且由于 unsigned long long(最大为 64C64)不够大(尽管事后看来,位域可能更好),因此位移位运算符重载。 【参考方案1】:

这是我为解决这个问题而开发的算法。它目前只是将每个组合输出为一系列 1 和 0,但可以轻松适应基于可能元素数组创建数据集。

void r_nCr(const unsigned int &startNum, const unsigned int &bitVal, const unsigned int &testNum) // Should be called with arguments (2^r)-1, 2^(r-1), 2^(n-1)

    unsigned int n = (startNum - bitVal) << 1;
    n += bitVal ? 1 : 0;

    for (unsigned int i = log2(testNum) + 1; i > 0; i--) // Prints combination as a series of 1s and 0s
        cout << (n >> (i - 1) & 1);
    cout << endl;

    if (!(n & testNum) && n != startNum)
        r_nCr(n, bitVal, testNum);

    if (bitVal && bitVal < testNum)
        r_nCr(startNum, bitVal >> 1, testNum);

工作原理:

此函数将元素的每个组合视为 1 和 0 的序列,然后可以相对于一组可能的元素来表示(但在此特定示例中没有)。

例如,3C2(3个可能元素的集合中length-2的所有组合)的结果可以表示为011、110和101。如果所有可能元素的集合是A, B, C ,那么关于这个集合的结果可以表示为B, C, A, B, and A, C。

为了这个解释,我将计算 5C3(所有长度为 3 的组合,由 5 个可能的元素组成)。

这个函数接受3个参数,都是无符号整数:

第一个参数是可能的最小整数,其二进制表示的 1 数等于我们正在创建的组合的长度。这是生成组合的起始值。对于 5C3,这将是 00111b,或十进制的 7。

第二个参数是起始编号中设置为1的最高位的值。这是创建组合时要减去的第一位。对于 5C3,这是从右数第三位,其值为 4。

第三个参数是右起第 n 位的值,其中 n 是我们正在组合的可能元素的数量。这个数字将与我们创建的组合进行按位与运算,以检查组合的最左侧位是 1 还是 0。对于 5C3,我们将使用右侧的第 5 位,即 10000b,或 16 in十进制。

以下是函数执行的实际步骤:

    计算startNum - bitVal,左移一位,如果bitVal不为0则加1。

对于第一次迭代,结果应该与 startNum 相同。这样我们就可以在函数中打印出第一个组合(等于 startNum),这样我们就不必提前手动完成。此操作的数学运算如下:

00111 - 00100 = 00011    
00011 << 1 = 00110   
00110 + 1 = 00111
    之前计算的结果是一个新的组合。用这些数据做点什么。

我们将把结果打印到控制台。这是使用 for 循环完成的,其变量开始等于我们正在使用的位数(通过获取 testNum 的 log2 并加 1 来计算;log2(16) + 1 = 4 + 1 = 5)并结束于0. 每次迭代,我们右移 i-1 位,并通过与 1 的结果与结果打印最右边的位。这是数学:

i=5:
00111 >> 4 = 00000
00000 & 00001 = 0

i=4:
00111 >> 3 = 00000
00000 & 00001 = 0

i=3:
00111 >> 2 = 00001
00001 & 00001 = 1

i=2:
00111 >> 1 = 00011
00011 & 00001 = 1

i=1:
00111 >> 0 = 00111
00111 & 00001 = 1

output: 00111
    如果 n 的最左边位(步骤 1 中的计算结果)为 0,并且 n 不等于 startNum,我们以 n 作为新的 startNum 进行递归。

显然这将在第一次迭代时被跳过,因为我们已经证明 n 等于 startNum。这在后续迭代中变得很重要,我们稍后会看到。

    如果 bitVal 大于 0 且小于 testNum,则以当前迭代的原始 startNum 作为第一个参数进行递归。第二个参数是 bitVal 右移 1(与整数除以 2 相同)。

我们现在将新的 bitVal 设置为当前 bitVal 右侧的下一位的值进行递归。下一位就是在下一次迭代中要减去的。

    继续递归,直到 bitVal 变为零。

因为在第二次递归调用中bitVal被右移一位,所以我们最终会到达bitVal等于0的点。这个算法展开成一棵树,当bitVal等于0且最左边的位为1时,我们从当前位置返回上一层。最终,这会一直级联到根。

在本例中,树有 3 个子树和 6 个叶节点。我现在将逐步介绍第一个子树,它由 1 个根节点和 3 个叶节点组成。

我们将从第一次迭代的最后一行开始,即

if (bitVal)
        r_nCr(startNum, bitVal >> 1, testNum);

所以我们现在进入第二次迭代,其中 startNum=00111(7)、bitVal = 00010(2) 和 testNum = 10000(16)(这个数字永远不会改变)。

第二次迭代

第 1 步:

n = 00111 - 00010 = 00101 // Subtract bitVal
n = 00101 << 1 = 01010 // Shift left
n = 01010 + 1 = 01011 // bitVal is not 0, so add 1

第 2 步:打印结果。

第3步:最左边的位为0,n不等于startNum,所以我们以n作为新的startNum进行递归。我们现在进入第三次迭代,startNum=01011(11),bitVal = 00010(2),testNum = 10000(16)。

第三次迭代

第 1 步:

n = 01011 - 00010 = 01001 // Subtract bitVal
n = 01001 << 1 = 10010 // Shift left
n = 10010 + 1 = 10011 // bitVal is not 0, so add 1

第 2 步:打印结果。

第3步:最左边的位是1,所以不要递归。

第 4 步:bitVal 不为 0,因此将 bitVal 右移 1 进行递归。我们现在进入第四次迭代,startNum=01011(11),bitVal = 00001(1),testNum = 10000(16)。

第四次迭代

第 1 步:

n = 01011 - 00001 = 01010 // Subtract bitVal
n = 01010 << 1 = 10100 // Shift left
n = 10100 + 1 = 10101 // bitVal is not 0, so add 1

第 2 步:打印结果。

第3步:最左边的位是1,所以不要递归。

第 4 步:bitVal 不为 0,因此将 bitVal 右移 1 进行递归。我们现在进入第五次迭代,startNum=01011(11),bitVal = 00000(0),testNum = 10000(16)。

第五次迭代

第 1 步:

n = 01011 - 00000 = 01011 // Subtract bitVal
n = 01011 << 1 = 10110 // Shift left
n = 10110 + 0 = 10110 // bitVal is 0, so add 0
// Because bitVal = 0, nothing is subtracted or added; this step becomes just a straight bit-shift left by 1.

第 2 步:打印结果。

第3步:最左边的位是1,所以不要递归。

第四步:bitVal为0,所以不要递归。

返回第二次迭代

第 4 步:bitVal 不为 0,因此递归使用 bitVal 右移 1。

这将一直持续到树的第一层的 bitVal = 0,然后我们返回第一次迭代,此时我们将完全从函数返回。

这是一个简单的图表,显示了函数的树状扩展:

这是一个更复杂的图表,显示了函数的执行线程:

这是一个替代版本,使用按位或代替加法和按位异或代替减法:

void r_nCr(const unsigned int &startNum, const unsigned int &bitVal, const unsigned int &testNum) // Should be called with arguments (2^r)-1, 2^(r-1), 2^(n-1)

    unsigned int n = (startNum ^ bitVal) << 1;
    n |= (bitVal != 0);

    for (unsigned int i = log2(testNum) + 1; i > 0; i--) // Prints combination as a series of 1s and 0s
        cout << (n >> (i - 1) & 1);
    cout << endl;

    if (!(n & testNum) && n != startNum)
        r_nCr(n, bitVal, testNum);

    if (bitVal && bitVal < testNum)
        r_nCr(startNum, bitVal >> 1, testNum);

【讨论】:

【参考方案2】:

这个怎么样?

#include <stdio.h>

#define SETSIZE 3
#define NELEMS  7

#define BYTETOBINARYPATTERN "%d%d%d%d%d%d%d%d"
#define BYTETOBINARY(byte)  \
    (byte & 0x80 ? 1 : 0), \
            (byte & 0x40 ? 1 : 0), \
            (byte & 0x20 ? 1 : 0), \
            (byte & 0x10 ? 1 : 0), \
            (byte & 0x08 ? 1 : 0), \
            (byte & 0x04 ? 1 : 0), \
            (byte & 0x02 ? 1 : 0), \
            (byte & 0x01 ? 1 : 0)

int main()

    unsigned long long x = (1 << SETSIZE) -1;
    unsigned long long N = (1 << NELEMS) -1;

    while(x < N)
    
            printf ("x: "BYTETOBINARYPATTERN"\n", BYTETOBINARY(x));
            unsigned long long a = x & -x;
            unsigned long long y = x + a;
            x = ((y & -y) / a >> 1) + y - 1;
    
;

它应该打印 7C3。

【讨论】:

但是如何调整组合集长度和可能元素的数量?

以上是关于如何(廉价)计算 n 个可能元素的所有可能的长度-r 组合的主要内容,如果未能解决你的问题,请参考以下文章

在C中创建n个项目的k和m个组合的所有可能子集[重复]

从n个整数列表(可能长度不等)中进行所有可能的n个长度排列[重复]

如何计算数字/位数组的所有可能性(在 python 或任何语言中)

有重复元素的排列问题

Python限定组合

生成真/假的所有长度-n 排列?