康托展开
标签: 数学方法——数论
阅读体验:https://zybuluo.com/Junlier/note/1174122
一、定义
来自网络的定义:
康托展开是一个全排列到一个自然数的双射,常用于构建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[j]在v[i]后面且比v[i]小(也就是
(v[j]<v[i]&&i<j)
)
那么这个“大”数字就一定会使这个排列的排名(康托展开值)造成影响(变大)- 根据上表的展开部分
位置在i位(这里是指倒过来的第i位)上的数后面有ss个比它小的数,那么我们可以认为它占领了!(i-1)*ss
个排列顺序。
为什么呢,拿出纸笔,随便找个例子,把它单独影响而占领的那几个排列列出来看一看就一目 了然了(我就是这么懂的)- 不可能马上就可以明白,对着上面的表格全部算一遍吧
再举个例子:
在(1,2,3,4,5)5个数的排列组合中,计算 34152的康托展开值。
- 首位是3,则小于3的数有两个,为1和2,a[5]=2,则首位小于3的所有排列组合为 a[0]*(5-1)!
- 第二位是4,则小于4的数有两个,为1和2,注意这里3并不能算,因为3已经在第一位,所以其实计算的是在第二位之后小于4的个数。因此a[4]=2
- 第三位是1,则在其之后小于1的数有0个,所以a[3]=0
- 第四位是5,则在其之后小于5的数有1个,为2,所以a[2]=1
- 最后一位就不用计算啦,因为在它之后已经没有数了,所以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全部是正着枚举的 //所以下面的jc处是n-i { 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;//标记用过 } } } //暴力到极致了吧……
四、题目
推荐板子:luoguP3014牛线
来源:https://www.cnblogs.com/cjoierljl/p/9147579.html