1. 程式人生 > >初學康託展開和逆康託展開

初學康託展開和逆康託展開

這篇部落格講的相當清楚了,轉載一下 1、簡述 康託展開是一個全排列到一個自然數的雙射,常用於構建hash表時的空間壓縮。設有n個數(1,2,3,4,…,n),可以有組成不同(n!種)的排列組合,康託展開表示的就是是當前排列組合在n個不同元素的全排列中的名次。

2、原理 X=a[n](n-1)!+a[n-1](n-2)!+…+a[i]*(i-1)!+…+a[1]*0! 其中, a[i]為整數,並且0 <= a[i] <= i, 0 <= i < n, 表示當前未出現的的元素中排第幾個,這就是康託展開。

例如有3個數(1,2,3),則其排列組合及其相應的康託展開值如下:

比如其中的 231: 想要計算排在它前面的排列組合數目(123,132,213),則可以轉化為計算算比首位小及小於2的所有排列「1 * 2!」,首位相等及為2第二位小於3的所有排列「11!」,前兩位相等及為23第三位小於1的所有排列(0

0!)的和即可,康託展開為:12!+11+0*0=3。 所以小於231的組合有3個,所以231的名次是4。 在這裡插入圖片描述

3、康託展開 再舉個例子說明。 在(1,2,3,4,5)5個數的排列組合中,計算 34152的康託展開值。

首位是3,則小於3的數有兩個,為1和2,a[5]=2,則首位小於3的所有排列組合為 a[0]*(5-1)! 第二位是4,則小於4的數有兩個,為1和2,注意這裡3並不能算,因為3已經在第一位,所以其實計算的是在第二位之後小於4的個數。因此a[4]=2 第三位是1,則在其之後小於1的數有0個,所以a[3]=0 第四位是5,則在其之後小於5的數有1個,為2,所以a[2]=1 最後一位就不用計算啦,因為在它之後已經沒有數了,所以a[1]固定為0 根據公式: X = 2 * 4! + 2 * 3! + 0 * 2! + 1 * 1! + 0 * 0! = 2 * 24 + 2 * 6 + 1 = 61

所以比 34152 小的組合有61個,即34152是排第62。

具體程式碼實現如下:(假設排列數小於10個)

static const int FAC[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};   // 階乘
int cantor(int *a, int n)
{
    int x = 0;
    for (int i = 0; i < n; ++i) {
        int smaller = 0;  // 在當前位之後小於其的個數
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[i])
                smaller++;
        }
        x += FAC[n - i - 1] * smaller; // 康託展開累加
    }
    return x;  // 康託展開值
}

tips: 這裡主要為了講解康託展開的思路,實現的演算法複雜度為O(n^2),實際當n很大時,內層迴圈計算在當前位之後小於當前位的個數可以用 線段樹來處理計算,而不用每次都遍歷,這樣複雜度可以降為O(nlogn)。

4、逆康託展開 一開始已經提過了,康託展開是一個全排列到一個自然數的雙射,因此是可逆的。即對於上述例子,在(1,2,3,4,5)給出61可以算出起排列組合為 34152。由上述的計算過程可以容易的逆推回來,具體過程如下:

用 61 / 4! = 2餘13,說明a[5]=2,說明比首位小的數有2個,所以首位為3。 用 13 / 3! = 2餘1,說明a[4]=2,說明在第二位之後小於第二位的數有2個,所以第二位為4。 用 1 / 2! = 0餘1,說明a[3]=0,說明在第三位之後沒有小於第三位的數,所以第三位為1。 用 1 / 1! = 1餘0,說明a[2]=1,說明在第二位之後小於第四位的數有1個,所以第四位為5。 最後一位自然就是剩下的數2啦。 通過以上分析,所求排列組合為 34152。

具體程式碼實現如下:(假設排列數小於10個)

static const int FAC[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};   // 階乘

//康託展開逆運算
void decantor(int x, int n)
{
    vector<int> v;  // 存放當前可選數
    vector<int> a;  // 所求排列組合
    for(int i=1;i<=n;i++)
        v.push_back(i);
    for(int i=m;i>=1;i--)
    {
        int r = x % FAC[i-1];
        int t = x / FAC[i-1];
        x = r;
        sort(v.begin(),v.end());// 從小到大排序 
        a.push_back(v[t]);      // 剩餘數裡第t+1個數為當前位
        v.erase(v.begin()+t);   // 移除選做當前位的數
    }
}

5、應用 應用最多的場景也是上述講的它的特性。

1>給定一個自然數集合組合一個全排列,所其中的一個排列組合在全排列中從大到小排第幾位。 2>在上述例子中,在(1,2,3,4,5)的全排列中,34152的排列組合排在第62位。 反過來,就是逆康託展開,求在一個全排列中,第n個全排列是多少。 比如求在(1,2,3,4,5)的全排列中,第62個排列組合是34152。[注意具體計算中,要先 -1 才是其康託展開的值。] 3>另外康託展開也是一個數組到一個數的對映,因此也是可用於hash,用於空間壓縮。比如在儲存一個序列,我們可能需要開一個數組,如果能夠把它對映成一個自然數, 則只需要儲存一個整數,大大壓縮空間。比如八數碼問題。