康托展開和逆康托展開
簡述
康托展開是一個全排列到一個自然數的雙射,常用於構建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,用於空間壓縮。比如在保存一個序列,我們可能需要開一個數組,如果能夠把它映射成一個自然數, 則只需要保存一個整數,大大壓縮空間。比如八數碼問題。