康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。可用于快速解决一些全排列问题。
康拓展开
第k个排列
给出一个数n,返回第k个排列。
假设n=3,k=3.那么第三个排列就是213
不难发现,当n=1的时候,有一个排列。11
当n=2的时候,有两个排列。12和21。21
当n=3的时候,根据上图可知有6种排列。321
可得:1…n有n!种排列。
相同的,当n=5的时候,求52413是第几个排列。
- 首先看首位5,当首位是5的时候,后面4个空位有4!排列的情况。且当首位是5的时候,按照从小到大的顺序而言,前面有1XXX,2XXX,3XXX,4XXX,所以5在首位前面有4x4!个排列。
- 那么当首位为5的情况确定后,第二位是2。后面三位有3!种排列情况。而又因为5XXXX,比二小的数字只有1。所以52XXX前面至少有 4x4!+1x3!中排列。
- 第三位是4,4后面还有两个空位。两个空位的排列数量是 2!。除去前2位用去的52,比4小的数字有1和3两个。所提524XXX前面至少有4x4!+1x3!+2x2!
- 第四位是1,按照前面的推理。5241X前面有4x4!+1x3!+2x2!+0x1!
- 第五位就定死了。0x0!
所以52413前面有4x4!+1x3!+2x2!+0x1!+0x0! = 106,所以52413是第107个排列。
同样的康拓展开12345是第几个排列。根据前面推到可得:0x4!+0x3!+0x2!+0x1!+0x0! = 0
所以12345 是第0个排列,康拓展开是从0开始的。
Java代码表示如下
public static void main(String[] args) {
System.out.println(calculateRank(52413));
}
//阶乘结果数组
static int[] arr = new int[]{1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
//计算某数字在多少位
//52413
public static int calculateRank(int num){
int res = 0;
String s = String.valueOf(num);
//遍历每一个数字计算结果
char[] chars = s.toCharArray();
int len = chars.length;
for (int i = 0; i < len; i++) {
int cnt = 0;
for (int j = i+1; j < len; j++) {
//如果后面的比前面的小
if (chars[i] > chars[j]){
cnt++;
}
}
//小的数字个数x后面的空格数的阶乘
//第0位后面有: 5 - 0 -1 = 4 个空位
res += cnt * arr[len - i -1];
}
return res+1;
}
逆康拓展开
既然康拓展开可以求某数字是全排列是多少位,那么顾名思义逆康拓展开可以求第几位全排列的数字具体的值。
同理求107在全排列结果集中对应的数字。
由于康拓展开从0开始,所以第107个对应的是106.
同样的给出1…5,求107对应的全排列数字。
- 首先确定第一位:当确定第一位的时候,后面四位的排列情况有4!=24种,107➗4 = 4余10,也就是说第一位用去了4个数字,所以第一位是5.
- 确定第二位:后面三位排列情况有 3! = 6. 10➗6 = 1余4,说明在剩余可选中(1,2,3,4)有一个数字比他小,这个数字是2.
- 确定第三位:后面两个空位 2! = 2 .4 ÷ 2 = 2余0 在(1,3,4)中,4满足这个条件。(有且仅有两个数字比他小)
- 确定第四位:后面一个空位。0➗ 1! = 0余0 所以这一位数 1.
- 所以最后一位是 3
Java代码
//阶乘结果数组
static int[] arr = new int[]{1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
public static String getNumByRank(int n,int k ){
List<Integer> candidates = new ArrayList<>(n);
for (int i = 1; i <=n ; i++) {
candidates.add(i);
}
k--;
StringBuilder sb = new StringBuilder();
for (int i = n-1; i >=0 ; i--) {
//计算有几个比它小的
int cnt = k / arr[i];
//当时1 2 3 4 5 索引 0 1 2 3 4
//计算出来有4个比他小,那么就是 index = 4 value = 5 此时数组-> 1 2 3 4
//达到了实时删除的结果比较最近最小满足
Integer num = candidates.remove(cnt);
sb.append(num);
k = k % arr[i];
}
return sb.toString();
}
如果不采用数学规律的方式如何解决全排列问题?
class Solution {
int[] nums;
boolean[] used;
public String getPermutation(int n, int k) {
nums =new int[n];
for (int i = 0; i < n; i++) {
nums[i] = i+1;
}
//记录是否使用过
used = new boolean[n];
List<String> list = new ArrayList<>();
return dfs(list,0,n,k);
}
private String dfs(List<String> levelList, int level, int n, int k) {
if (level == n){
StringBuilder res = new StringBuilder();
for (String s : levelList) {
res.append(s);
}
return res.toString();
}
//比如来到了确定第一位的数字 我们假设n=5 确定一位有 (5 - 0 -1 )! 种情况
int curCount = factorial(n - level - 1);
//组合每一位 我这里是按照索引取值
for (int i = 0; i < n; i++) {
if (used[i]){
continue;
}
if (curCount < k){
k = k - curCount;
continue;
}
//剩下的就是确定结果的时候啦
levelList.add(nums[i]+"");
used[i] = true;
//在以往的回溯中 会把标记还原 但是此处不同,因为我们直接确定的就是最后一层
return dfs(levelList,level+1,n,k);
}
return null;
}
private int factorial(int n) {
int res = 1;
while (n > 0) {
res *= n--;
}
return res;
}
}
此外,康托展开也是一个数组到一个数的映射,可以应用于hash中进行空间压缩。例如,在八数码问题中,我们可以把一种排列状态压缩成一个整数存放在数组中。