繁体   English   中英

如何(便宜地)计算n个可能元素的所有可能的length-r组合

[英]How to (cheaply) calculate all possible length-r combinations of n possible elements

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

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

我出于好奇心要求的更多,因为我目前拥有的算法可以很好地满足我的目的。

这是我为解决此问题而开发的算法。 它当前仅将每个组合输出为一系列的一和零,但是可以很容易地调整为根据可能的元素数组创建数据集。

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);
}

这个怎么运作:

此函数将元素的每种组合视为一串零序列,然后可以相对于一组可能的元素表示(但在此特定示例中不是)。

例如,3C2的结果(来自3个可能元素的长度2的所有组合)可以表示为011、110和101。如果所有可能元素的集合为{A,B,C},则结果可以针对此集合表示为{B,C},{A,B}和{A,C}。

为了便于说明,我将计算5C3(由5个可能的元素组成的所有长度为3的组合)。

此函数接受3个参数,所有参数均为无符号整数:

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

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

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

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

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

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

00111 - 00100 = 00011    
00011 << 1 = 00110   
00110 + 1 = 00111
  1. 先前计算的结果是新的组合。 使用此数据做些事情。

我们将把结果打印到控制台。 这是使用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
  1. 如果n的最左位(步骤1中的计算结果)为0并且n不等于startNum,则以n作为新的startNum递归。

显然,这将在第一次迭代时跳过,因为我们已经表明n等于startNum。 这在随后的迭代中变得很重要,我们将在后面看到。

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

现在,我们将新的bitVal设置为当前bitVal右边的下一位的值来递归。 下一位是在下一次迭代中减去的位。

  1. 继续递归直到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,所以不要递归。

步骤4:bitVal为0,因此请勿递归。

返回第二个迭代

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

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

这是显示函数树状扩展的简单图: 该图显示了递归扩展

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

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

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);
}

那这个呢?

#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。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM