aq`QWBNz# 康托展开
康托展开是一个全排列到一个自然数的映射。
通俗简介
康托展开可以求解一个全排列在所有全排列中的次序,比如:(如果规定)排列12345(在1~5的所有全排列中)次序为1 ,(则)12354次序为2,按字典序增加编号递增,依次类推。
具体原理就不用术语来解释了,直接上栗子。
栗子
给定一个全排列,计算其字典序。直观起见,我们举例[2, 3, 4, 5,1]来说明康托展开的运算步骤(设其字典序为rank=0):
-
因为[2, 3, 4, 5,1]的首位为2,所以第一位为1的所有全排列一定在[2, 3, 4, 5,1]的后面。这种全排列共有1×4!种;
-
当首位固定为2时,因为全排列中不能出现重复数字(2已经被用掉了),所以第二位为1的所有全排列一定在[2, 3, 4, 5,1]的后面,这种全排列共有1×3!种;
-
当首位固定为2,第二位固定为3时,(2、3已经被用掉了)所以第三位为1的所有全排列一定在[2, 3, 4, 5,1]的后面,这种全排列共有1×2!种;
-
当首位固定为2,第二位固定为3,第三位固定为4时,(2、3、4已经被用掉了)所以第四位为1的所有全排列一定在[2, 3, 4, 5,1]的后面,这种全排列共有1×1!种;
-
当首位固定为2,第二位固定为3,第三位固定为4,第四位固定为5时,(2、3、4、5已经被用掉了)所以此时没有任何全排列在[2, 3, 4, 5,1]的后面,这种全排列共有0×0!种;
-
所以[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!的系数。
- 因为序列有6位数,所以n=6。先用5!整除186,商为1,余数为66。商为1说明第1位到第5位中只有1位数比第6位数小,所以第6位数为2。
- 用4!整除第1步得到的余数66,得商为2,余数18。商为2说明第1位到第4位中有2位数比第5位数小。因为第6位已经确定为2了,所以第5位数为4。
- 用3!整除第2步得到的余数18,得商为3,余数0。商为3说明第1位到第3位中有3位数比第4位数小。因为2、4已经被用过了,所以第4位数为6。
- 用2!整除第3步得到的余数0,得到商0,余数0。说明第1位到第2位中没有数比第3位数小。因为2、4、6已经被用过了,所以第3位数为1。
- 用1!整除第4步得到的余数0,得到商0,余数0。说明第1位比第2位数大。因为2、4、6、1已经被用过了,所以第2位数为3。
- 第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;
}