簡體   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