描述:
\quad 康托展开是一个全排列到一个自然数的双射,常用于构建 h a s h hash hash表时的空间压缩。设有 n n n个数 ( 1 , 2 , 3 , 4 , … , n ) (1,2,3,4,…,n) (1,2,3,4,…,n),可以有组成不同( n ! n! n!种)的排列组合,康托展开表示的就是是当前排列组合在 n n n个不同元素的全排列中的名次。
原理:
X = a n ∗ ( n − 1 ) ! + a n − 1 ∗ ( n − 2 ) ! + . . . + a i ∗ ( i − 1 ) ! + . . . + a 1 ∗ 0 ! X=a_{n}*(n-1)!+a_{n-1}*(n-2)!+...+a_{i}*(i-1)!+...+a_{1}*0! X=an∗(n−1)!+an−1∗(n−2)!+...+ai∗(i−1)!+...+a1∗0!
其中, a i a_{i} ai为整数,并且 0 < = a i < = i , 0 < = i < n , 0<= a_{i} <= i, 0 <= i < n, 0<=ai<=i,0<=i<n, 表示当前未出现的的元素中排第几个,这就是康托展开。
例如有 3 3 3个数 ( 1 , 2 , 3 ) (1,2,3) (1,2,3),则其排列组合及其相应的康托展开值如下:
| 排列组合 | 名次 | 康托展开 |
|---|---|---|
| 123 | 1 | 0 * 2! + 0 * 1! + 0 * 0! |
| 132 | 2 | 0 * 2! + 1 * 1! + 0 * 0! |
| 213 | 3 | 1 * 2! + 0 * 1! + 0 * 0! |
| 231 | 4 | 1 * 2! + 1 * 1! + 0 * 0! |
| 312 | 5 | 2 * 2! + 0 * 1! + 0 * 0! |
| 231 | 6 | 2 * 2! + 1 * 1! + 0 * 0! |
比如其中的 231:
- 想要计算排在它前面的排列组合数目 ( 123 , 132 , 213 ) (123,132,213) (123,132,213),则可以转化为计算比首位小即小于 2 2 2的所有排列「 1 ∗ 2 ! 」 1 * 2!」 1∗2!」,首位相等为2并且第二位小于3的所有排列 「 1 ∗ 1 ! 」 「1 * 1!」 「1∗1!」,前两位相等为23并且第三位小于1的所有排列 ( 0 ∗ 0 ! ) (0 *0!) (0∗0!)的和即可,康托展开为: 1 ∗ 2 ! + 1 ∗ 1 + 0 ∗ 0 = 3 。 1 * 2!+1 * 1+0 * 0=3。 1∗2!+1∗1+0∗0=3。
- 所以小于 231 231 231的组合有 3 3 3个,所以 231 231 231的名次是 4 4 4。
康托展开
再举个例子说明。
在 ( 1 , 2 , 3 , 4 , 5 ) 5 (1,2,3,4,5)5 (1,2,3,4,5)5个数的排列组合中,计算 34152 34152 34152的康托展开值。
首位是 3 3 3,则小于 3 3 3的数有两个,为 1 1 1和 2 2 2, a 5 = 2 a_{5}=2 a5=2,则首位小于 3 3 3的所有排列组合为 a 5 ∗ ( 5 − 1 ) ! a_{5}*(5-1)! a5∗(5−1)!
第二位是 4 4 4,则小于$$4的数有两个,为 1 1 1和 2 2 2,注意这里 3 3 3并不能算,因为 3 3 3已经在第一位,所以其实计算的是在第二位之后小于 4 4 4的个数。因此 a 4 = 2 a_{4}=2 a4=2
第三位是 1 1 1,则在其之后小于 1 1 1的数有 0 0 0个,所以 a 3 = 0 a_{3}=0 a3=0
第四位是 5 5 5,则在其之后小于 5 5 5的数有 1 1 1个,为 2 2 2,所以 a 2 = 1 a_{2}=1 a2=1
最后一位就不用计算啦,因为在它之后已经没有数了,所以 a 1 a_{1} a1固定为 0 0 0
根据公式:
X = 2 ∗ 4 ! + 2 ∗ 3 ! + 0 ∗ 2 ! + 1 ∗ 1 ! + 0 ∗ 0 ! = 2 ∗ 24 + 2 ∗ 6 + 1 = 61 X = 2 * 4! + 2 * 3! + 0 * 2! + 1 * 1! + 0 * 0! = 2 * 24 + 2 * 6 + 1 = 61 X=2∗4!+2∗3!+0∗2!+1∗1!+0∗0!=2∗24+2∗6+1=61
所以比 34152 34152 34152 小的组合有 61 61 61个,即 34152 34152 34152是排第 62 62 62。
具体代码实现如下:(假设排列数小于 10 10 10个)
Code:
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;/// 康托展开值
}
\quad 这里主要为了讲解康托展开的思路,实现的算法复杂度为 O ( n 2 ) O(n^2) O(n2),实际当 n n n很大时,内层循环计算在当前位之后小于当前位的个数可以用 线段树来处理计算,而不用每次都遍历,这样复杂度可以降为 O ( n l o g n ) O(nlogn) O(nlogn)。
逆康托展开
\quad 一开始已经提过了,康托展开是一个全排列到一个自然数的双射,因此是可逆的。即对于上述例子,在 ( 1 , 2 , 3 , 4 , 5 ) (1,2,3,4,5) (1,2,3,4,5)给出 61 61 61可以算出起排列组合为 34152 34152 34152。由上述的计算过程可以容易的逆推回来,具体过程如下:
用 61 / 4 ! = 2 61 / 4! = 2 61/4!=2余 13 13 13,说明 a 5 = 2 a_{5}=2 a5=2,说明比首位小的数有 2 2 2个,所以首位为 3 3 3。
用 13 / 3 ! = 2 13 / 3! = 2 13/3!=2余 1 1 1,说明 a 4 = 2 a_{4}=2 a4=2,说明在第二位之后小于第二位的数有 2 2 2个,所以第二位为 4 4 4。
用 1 / 2 ! = 0 1 / 2! = 0 1/2!=0余 1 1 1,说明 a 3 = 0 a_{3}=0 a3=0,说明在第三位之后没有小于第三位的数,所以第三位为 1 1 1。
用 1 / 1 ! = 1 1 / 1! = 1 1/1!=1余 0 0 0,说明 a 2 = 1 a_{2}=1 a2=1,说明在第二位之后小于第四位的数有 1 1 1个,所以第四位为 5 。 5。 5。
最后一位自然就是剩下的数 2 2 2啦。
通过以上分析,所求排列组合为 34152 。 34152。 34152。
具体代码实现如下:(假设排列数小于 10 10 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 ) (1,2,3,4,5) (1,2,3,4,5)的全排列中, 34152 34152 34152的排列组合排在第 62 62 62位。 - 反过来,就是逆康托展开,求在一个全排列中,从小到大的第 n n n个全排列是多少。
比如求在 ( 1 , 2 , 3 , 4 , 5 ) (1,2,3,4,5) (1,2,3,4,5)的全排列中,第 62 62 62 个排列组合是 34152 34152 34152。[注意具体计算中,要先 − 1 -1 −1才是其康托展开的值。 - 另外康托展开也是一个数组到一个数的映射,因此也是可用于 h a s h hash hash,用于空间压缩。比如在保存一个序列,我们可能需要开一个数组,如果能够把它映射成一个自然数,
则只需要保存一个整数,大大压缩空间。比如八数码问题。