康托展开(数论)

一、定义

来自网络的定义:

康托展开是一个全排列到一个自然数的双射,常用于构建hash表时的空间压缩。设有n个数(1,2,3,4,…,n),可以有组成不同(n!种)的排列组合,康托展开表示的就是是当前排列组合在n个不同元素的全排列中的名次。

通俗来讲:

假设有一个排列{1,2,3,4,5},需要你在它的全排列中,找到排名第m的那个排列(其实全排列的顺序很简单,就是字典序越来越大的排列,和我们的next_permutation()函数的顺序一样)

二、怎么实现?

首先,有一个很重要的公式(暂时先不要理解,后面慢慢就懂了):
X=a[n](n-1)!+a[n-1](n-2)!+...+a[i](i-1)!+...+a[1]0!(!是阶乘)
先看一个表格(数据来自网络)
你先不管它的康拓展开那一栏,根据后面的讲解再看
|排列组合 |名次 |康托展开|
|:-----: |:-----: |:-----: |
|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!|
|321 |6 |2 * 2! + 1 * 1! + 0 * 0!|
思考一下
我们当前数列排在第x位是不是因为有大数字占领了前面的小数字的位置,这个可以理解
那么,我们来考虑每个大数字对整个排列的编号的影响

  • 如果有一个在v[i]后面且比v[i]小(也就是(v[j]<v[i]&&i<j)),那么这个“大”数字就一定可以对这个排列的位置造成影响
  • 位置在i位(这里指个(i=1),十(i=2),百(i=3)……)(可以看上表的展开部分了)上的数后面有ss个比它小的数,那么我们可以认为它占领了!(i-1)*ss个排列顺序。为什么呢,拿出纸笔,随便找个例子,把它单独影响而占领的那几个排列列出来看一看就一目了然了(我就是这么懂的)
  • 不可能马上就可以明白,对着上面的表格全部算一遍吧

再举个例子:
在(1,2,3,4,5)5个数的排列组合中,计算 34152的康托展开值。

  1. 首位是3,则小于3的数有两个,为1和2,a[5]=2,则首位小于3的所有排列组合为 a[0]*(5-1)!
  2. 第二位是4,则小于4的数有两个,为1和2,注意这里3并不能算,因为3已经在第一位,所以其实计算的是在第二位之后小于4的个数。因此a[4]=2
  3. 第三位是1,则在其之后小于1的数有0个,所以a[3]=0
  4. 第四位是5,则在其之后小于5的数有1个,为2,所以a[2]=1
  5. 最后一位就不用计算啦,因为在它之后已经没有数了,所以a[1]固定为0根据公式:
    X = 2 * 4! + 2 * 3! + 0 * 2! + 1 * 1! + 0 * 0!
    = 2 * 24 + 2 * 6 + 1
    = 61

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

代码实现就很简单了(其实找后面有几个比v[i]小有各种方法log(n)!)

    for(int i=1;i<=n;++i)//注意这里i全部变了
    //第n个是个位,n-1是十位,以此类推
    {
        int ss=0;//意思同上
        for(rg int j=i+1;j<=n;++j)//找后面比v[i]小的
            if(v[j]<v[i])ss++;
        num+=ss*jc[n-i];//jc是阶乘(预处理好的数组)
    }
    num++;//加上1是显然的(61个在我前面,那我就排62)

三、补充:逆康托展开

其实和康拓展开差不多,总体思想很简单,再用一下上面的例子,我们通过34152的一系列计算得到了62,那我们肯定可以根据62倒退回去,具体如下:

  • 首先肯定把62减回去到61才好算 ^_^
  • 然后:
    1.用61/(!4)=2余13,说明a[5]=2,说明比首位小的数有2个,所以首位为3。
    2.用13/(!3)=2余1,说明a[4]=2,说明在第二位之后小于第二位的数有2个,所以第二位为4。
    3.用1/(!2)=0余1,说明a[3]=0,说明在第三位之后没有小于第三位的数,所以第三位为1。
    4.用1/(!1)=1余0,说明a[2]=1,说明在第二位之后小于第四位的数有1个,所以第四位为5。
    5.最后一位自然就是剩下的数2啦。
    6.通过以上分析,所求排列组合为 34152。

恩,那个代码我打的纯暴力

    num--;
    for(rg int i=1;i<n;++i)
    {
        rg int kk=num/jc[n-i]+1;//向上面讲解里那样计算
        //+1不用解释吧,x个比我小,我就是第x+1个
        rg int z=0;//计录当前到第几小了
        num=num%jc[n-i];//同上
        for(rg int j=1;j<=n;++j)//找没有用过(没有排在左边)第kk小的数
        {
            if(!b[j])z++;//没用过
            if(z==kk)
            {
                printf("%d ",j);//就是你了
                b[j]=1;break;//标记用过
            }
        }
    }
    //暴力到极致了吧……

四、题目

推荐板子:luoguP2525

猜你喜欢

转载自www.cnblogs.com/cjoierljl/p/9147579.html
今日推荐