算法基础知识:康托展开与康托逆展开

aq`QWBNz# 康托展开

康托展开是一个全排列到一个自然数的映射。

通俗简介

康托展开可以求解一个全排列在所有全排列中的次序,比如:(如果规定)排列12345(在1~5的所有全排列中)次序为1 ,(则)12354次序为2,按字典序增加编号递增,依次类推。
在这里插入图片描述
具体原理就不用术语来解释了,直接上栗子。

栗子

给定一个全排列,计算其字典序。直观起见,我们举例[2, 3, 4, 5,1]来说明康托展开的运算步骤(设其字典序为rank=0):

  1. 因为[2, 3, 4, 5,1]的首位为2,所以第一位为1的所有全排列一定在[2, 3, 4, 5,1]的后面。这种全排列共有1×4!种;
    在这里插入图片描述

  2. 当首位固定为2时,因为全排列中不能出现重复数字(2已经被用掉了),所以第二位为1的所有全排列一定在[2, 3, 4, 5,1]的后面,这种全排列共有1×3!种;
    在这里插入图片描述

  3. 当首位固定为2,第二位固定为3时,(2、3已经被用掉了)所以第三位为1的所有全排列一定在[2, 3, 4, 5,1]的后面,这种全排列共有1×2!种;
    在这里插入图片描述

  4. 当首位固定为2,第二位固定为3,第三位固定为4时,(2、3、4已经被用掉了)所以第四位为1的所有全排列一定在[2, 3, 4, 5,1]的后面,这种全排列共有1×1!种;
    在这里插入图片描述

  5. 当首位固定为2,第二位固定为3,第三位固定为4,第四位固定为5时,(2、3、4、5已经被用掉了)所以此时没有任何全排列在[2, 3, 4, 5,1]的后面,这种全排列共有0×0!种;

  6. 所以[2, 3, 4, 5,1]的次序rank=1×4!+1×3!+1×2!+1×1!+0×0!+1=33+1=34.

为什么最后要加一?因为算出来的1×4!+1×3!+1×2!+1×1!+0×0!(=33)是排在[2, 3, 4, 5,1]后面的全排列的个数,所以[2, 3, 4, 5,1]是第34位

特别注意:[ 1,2,3,4,5 ]的康托展开值为0×4!+ 0×3!+ 0×2!+ 0×1!+0×0! = 0(康托公式最小字典序的编号就是0) ,即有0个全排列在[ 1,2,3,4,5 ]的后面,所以[ 1,2,3,4,5 ]是第1位。

公式

在这里插入图片描述在这里插入图片描述这里的 X 就是该全排列的康托展开值,X+1 就是该全排列的次序。n为全排列中元素的个数。如果定义全排列的右边为第一位,左边为第n位,则在这里插入图片描述
代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int n,a[25];//n为全排列的位数,a[i]为康托展开计算公式中的ai
int permutation_rank;//permutation_rank为输入全排列所占的次序
int result[25],permutation[25];//数组result存储每一位阶乘的结果,数组permutation存储输入的全排列


int factorial()//计算阶乘
{
    int i;
    result[0]=result[1]=1;//0和1的阶乘为1
    for(i=2;i<=n;i++)
    {
        result[i]=i*result[i-1];
    }
    return 0;
}

int Cantor_expansion()//计算排列的康托展开值
{
    int i,j;
    for(i=0;i<n;i++)
    {
        for(j=i+1;j<n;j++)
        {
            if(permutation[j]<permutation[i])
            {
                a[i]++;
            }
        }
        permutation_rank+=a[i]*result[n-i-1];
    }
    return 0;
}

int main()
{
    int i;
    char str[30],b;//字符b用于抵消回车键
    //输入数据
    printf("n=");
    scanf("%d%c",&n,&b);
    scanf("%s",str);
    for(i=0;i<n;i++)//将输入的字符转换为数字
    {
        permutation[i]=str[i]-48;
    }
    //计算全排列的次序
    factorial();
    permutation_rank=1;//12345的康托展开值为0,但我们是将它的次序看作1的
    Cantor_expansion();
    //输出结果
    for(i=0;i<n;i++)
    {
        printf("%d",permutation[i]);
    }
    printf("=");
    for(i=0;i<n-1;i++)
    {
        printf("%d×%d+",a[i],result[n-i-1]);
    }
    printf("%d×%d+1=",a[n-1],result[0]);
    printf("%d\n",permutation_rank);
    return 0;
}

在这里插入图片描述

										~~手动分割~~ 

康托逆展开

康托逆展开可以求解一个次序号对应的全排列是什么。
次序减一即得到一个康托展开值,通过变换这个展开值即可得到该次序对应的全排列。

栗子

如果初始序列是123456(第一个),让你求第187个序列是什么。(按字典序递增)

规定全排列最左边为第n位,最右边为第1位。

先将187减一得到该序列的康托展开值186。
在这里插入图片描述由康托展开的公式知,康托展开值必须为(n-1)!、(n-2)!、……、2!、1!、0!的线性组合。因此可通过对康托展开值X不断整除取余从而得到(n-1)!、(n-2)!、……、2!、1!、0!的系数。

  1. 因为序列有6位数,所以n=6。先用5!整除186,商为1,余数为66。商为1说明第1位到第5位中只有1位数比第6位数小,所以第6位数为2。在这里插入图片描述
  2. 用4!整除第1步得到的余数66,得商为2,余数18。商为2说明第1位到第4位中有2位数比第5位数小。因为第6位已经确定为2了,所以第5位数为4。在这里插入图片描述
  3. 用3!整除第2步得到的余数18,得商为3,余数0。商为3说明第1位到第3位中有3位数比第4位数小。因为2、4已经被用过了,所以第4位数为6。在这里插入图片描述
  4. 用2!整除第3步得到的余数0,得到商0,余数0。说明第1位到第2位中没有数比第3位数小。因为2、4、6已经被用过了,所以第3位数为1。在这里插入图片描述
  5. 用1!整除第4步得到的余数0,得到商0,余数0。说明第1位比第2位数大。因为2、4、6、1已经被用过了,所以第2位数为3。在这里插入图片描述
  6. 第1位数为5。在这里插入图片描述
    验证一下,246135的次序为在这里插入图片描述
    代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int visit[30];//数组visit记录每一位数是否被取用过
int n;//n为全排列的位数
int result[25],permutation[25];//数组result存储每一位阶乘的结果,数组permutation存储输出的全排列

int factorial()//计算阶乘
{
    int i;
    result[0]=result[1]=1;//0和1的阶乘为1
    for(i=2;i<=n;i++)
    {
        result[i]=i*result[i-1];
    }
    return 0;
}

int Cantor_inverse_expansion(int Cantor_number)//康托逆展开
{//Cantor_number为全排列的康托展开值
    int quotient=0;//quotient意为商
    int sum=0;//sum用于记录小于等于商且未被取用的数字的个数
    int i,j,remainder=Cantor_number;//remainder意为余数,存储每次整除运算后得到的余数
    for(i=0;i<n;i++)
    {
        sum=0;//每次都要将计数器清零
        quotient=remainder/result[n-1-i];
        remainder=remainder%result[n-1-i];
        if(!quotient)//若商为0
        {
            for(j=1;j<=n;j++)
            {
                if(!visit[j])//此时的j一定是剩余未被取用的数字中最小的
                {
                    permutation[i]=j;//将j放入数组
                    visit[j]=1;
                    break;//跳出内层循环
                }
            }
        }
        else//若商不为0
        {
            for(j=1;j<=n;j++)
            {
                if(!visit[j])//若数字j还没被取用过
                {
                    sum++;
                }
                if(sum==(quotient+1))
                {
                    permutation[i]=j;//所求全排列的第i位确定
                    visit[j]=1;//数字j已被取用
                    break;//跳出内层循环
                }
             }
        }
    }
    return 0;
}

//康托逆展开
int main()
{
    int i,order;//order为输入的某个全排列的次序
    //输入数据
    printf("n=");
    scanf("%d",&n);
    printf("order=");
    scanf("%d",&order);
    //寻找全排列
    factorial();
    Cantor_inverse_expansion(order-1);//次序减一即为全排列的康托展开值
    //输出结果
    for(i=0;i<n;i++)
    {
        printf("%d",permutation[i]);
    }
    return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值