康托展及其逆展开

179 阅读2分钟

描述:

\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),则其排列组合及其相应的康托展开值如下:

排列组合名次康托展开
12310 * 2! + 0 * 1! + 0 * 0!
13220 * 2! + 1 * 1! + 0 * 0!
21331 * 2! + 0 * 1! + 0 * 0!
23141 * 2! + 1 * 1! + 0 * 0!
31252 * 2! + 0 * 1! + 0 * 0!
23162 * 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,用于空间压缩。比如在保存一个序列,我们可能需要开一个数组,如果能够把它映射成一个自然数,
    则只需要保存一个整数,大大压缩空间。比如八数码问题。