康托展開和逆康托展開(轉)


康托展開和逆康托展開

簡述
康托展開是一個全排列到一個自然數的雙射,常用於構建hash表時的空間壓縮。設有n個數(1,2,3,4,…,n),可以有組成不同(n!種)的排列組合,康托展開表示的就是是當前排列組合在n個不同元素的全排列中的名次。

原理
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的所有排列「1 * 1!」,前兩位相等為23並且第三位小於1的所有排列(0 * 0!)的和即可,康托展開為:1 * 2!+1 * 1+0 * 0=3。
所以小於231的組合有3個,所以231的名次是4。

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

首位是3,則小於3的數有兩個,為1和2,a[5]=2,則首位小於3的所有排列組合為 a[5]*(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;  // 康托展開值
}

逆康托展開
一開始已經提過了,康托展開是一個全排列到一個自然數的雙射,因此是可逆的。即對於上述例子,在(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);   // 移除選做當前位的數
    }
}

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

給定一個自然數集合組合一個全排列,所其中的一個排列組合在全排列中從小到大排第幾位。
在上述例子中,在(1,2,3,4,5)的全排列中,34152的排列組合排在第62位。

反過來,就是逆康托展開,求在一個全排列中,從小到大的第n個全排列是多少。
比如求在(1,2,3,4,5)的全排列中,第62個排列組合是34152。[注意具體計算中,要先 -1 才是其康托展開的值。]

另外康托展開也是一個數組到一個數的映射,因此也是可用於hash,用於空間壓縮。比如在保存一個序列,我們可能需要開一個數組,如果能夠把它映射成一個自然數, 則只需要保存一個整數,大大壓縮空間。比如八數碼問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM