康托展开总结

康托展开

一、康托展开

1. 定义

康托展开是一个全排列到一个自然数的双映射,常用于构建哈希表时的空间压缩;

设有 n n n 个数 ( 1 , 2 , 3 , 4 , . . . , n ) (1, 2, 3, 4, ... , n) (1,2,3,4,...,n) ,可以组成 n ! n! n! 种不同的排列组合,康托展开表示的就是当前排列组合再所有全排列中的字典序名次;

康托展开的实质是计算当前排列在所有的由小到大全排列中的顺序,因此是可逆的;

2. 原理

设有排列 p = a 1 a 2 . . . a n p = a_1 a_2 ... a_n p=a1a2...an ,则对于字典序比 p p p 小的排列一定存在排列 p 1 p_1 p1 ,使得排列的前 i − 1 i - 1 i1 位与 p p p 相同,第 i i i 位比 p i p_i pi 小,后续位随意;

对于任意 i i i ,满足条件的排列数就是从 n − i + 1 n - i + 1 ni+1 位中选一个比 a i a_i ai 小的数,并将剩下的 n − i n - i ni 个数随意排列,则方案数为 A i ∗ ( n − i ) ! A_i * (n - i)! Ai(ni)! (其中 A i A_i Ai 表示 a i a_i ai 后面比 a i a_i ai 小的数的个数);

则总的方案数即为 Σ i = 1 n − 1 A i ∗ ( n − i ) ! \Sigma_{i = 1}^{n - 1} A_i * (n - i)! Σi=1n1Ai(ni)! ,再加 1 即为排名;

关于求 A i A_i Ai ,可以用 O ( n 2 ) O(n^2) O(n2) 求,也可以用树状数组优化到 O ( n l o g n ) O(nlog_n) O(nlogn)

3. 例子

p = 4 , 1 , 3 , 2 p = 4, 1, 3, 2 p=4,1,3,2 ,可以求得 A = [ 3 , 0 , 1 , 0 ] A = [3, 0, 1, 0] A=[3,0,1,0] ,则;

第一位比 p 1 p_1 p1 小的排列数为 3 ∗ 3 ! = 18 3 * 3! = 18 33!=18

第一位与 p 1 p_1 p1 相等,第二位比 p 2 p_2 p2 小的排列数为 0 0 0

第一,二位分别与 p 1 , p 2 p_1, p_2 p1,p2 相等,第三位比 p 3 p_3 p3 小的排列数为 1 ∗ 1 ! = 1 1 * 1! = 1 11!=1

所以 p p p 的排名是 18 + 0 + 1 + 1 = 20 18 + 0 + 1 + 1 = 20 18+0+1+1=20

4. 代码

预处理

预处理出阶乘与 A A A 数组

O ( n 2 ) O(n^2) O(n2) 写法

void firstset(int n) {
    fc[1] = 1;
	for (int i = 2; i <= n; i++) {
		fc[i] = (fc[i - 1] * i);
	}
	for (int i = 1; i <= n; i++) {
		for (int j = i + 1; j <= n; j++) {
			if (a[j] < a[i]) {
				b[i]++;
			}
		}
	}
	return;
}

树状数组写法

int lowbit(int x) { 
	return x & (-x);
}
void add(int x, int a) {
	while (x <= n) {
		c[x] += a;
		x += lowbit(x);
	}
	return;
}
int query(int x) {
	int ans = 0;
	while (x != 0) {
		ans += c[x];
		x -= lowbit(x);
	}
	return ans;
}
void firstset(int n, int a[]) {
	fc[1] = 1;
	for (int i = 2; i <= n; i++) {
		fc[i] = (fc[i - 1] * i);
	}
	for (int i = n; i >= 1; i--) {
		b[i] = query(a[i]); // 前缀和查询,查询比 a[i] 小的数字数量
		add(a[i], 1); // 单点修改
	}
	return;
}
康托展开
int cantor(int n, int a[]) {
	int ans = 1;
	for (int i = 1; i < n; i++) {
		ans += b[i] * fc[n - i];
	}
	return ans;
}

三、逆康托展开

1. 定义

逆康托展开即为指定排名求排列;

2. 原理

n ! = n ∗ ( n − 1 ) ! = ( n − 1 ) ∗ ( n − 1 ) ! + ( n − 1 ) ! n! = n * (n - 1)! = (n - 1) * (n - 1)! + (n - 1)! n!=n(n1)!=(n1)(n1)!+(n1)!

继续展开得

n ! = Σ i = 1 n − 1 ( i ∗ i ! ) + 1 n! = \Sigma_{i = 1}^{n - 1}(i * i!) + 1 n!=Σi=1n1(ii!)+1

n ! > Σ i = 1 n − 1 ( i ∗ i ! ) n! > \Sigma_{i = 1}^{n - 1}(i * i!) n!>Σi=1n1(ii!)

n ! > ( n − 1 ) ∗ ( n − 1 ) ! + ( n − 2 ) ∗ ( n − 2 ) ! + . . . + 2 ∗ 2 ! + 1 ∗ 1 ! n! > (n - 1) * (n - 1)! + (n - 2) * (n - 2)! + ... + 2 * 2! + 1 * 1! n!>(n1)(n1)!+(n2)(n2)!+...+22!+11!

由于每一项的 ( n − i ) ! (n - i)! (ni)! 都比后面所有项的总和还大,则可以用类似进制转化的方法,不断除模得到得到 A A A 数组;

得到 A A A 数组后,即可得到 p i p_i pi 就是剩余未用的数中第 A i + 1 A_i + 1 Ai+1 小的;

3. 例子

1 , 2 , 3 , 4 , 5 {1, 2, 3, 4, 5} 1,2,3,4,5 的排列为例,求第 96 个排列;

首先 96 − 1 = 95 96 - 1 = 95 961=95

95 / 4 ! = 3......23 95 / 4! = 3 ...... 23 95/4!=3......23 ,可知比它小的数有 3 个,即为 4 ;

23 / 3 ! = 3......5 23 / 3! = 3 ...... 5 23/3!=3......5 ,可知比它小的数有 2 个,因为 4 已经出现过,所以为 5 ;

5 / 2 ! = 2......1 5 / 2! = 2 ...... 1 5/2!=2......1 ,可知比它小的数有 2 个,即为 3 ;

1 / 1 ! = 1 1 / 1! = 1 1/1!=1 ,可知只有 2 符合;

最后一位就是剩下的 1 ;

最终结果为 45321 ;

4. 代码

预处理

预处理出阶乘

void firstset(int n) {
	fc[0] = fc[1] = 1;
	for (int i = 2; i <= n; i++) {
		fc[i] = i * fc[i - 1];
	}
	return;
}
逆康托展开
void inverse_cantor(int m, int n) { // m 为排名,n 为排列的长度
	vector < int > v; // 存放当前可选数
	vector < int > a; // 所求排列
	int x = m - 1;
	for (int i = 1; i <= n; i++) {
		v.push_back(i);
	}
	for (int i = n; i >= 1; i--) {
		int r = x % fc[i - 1];
		int t = x / fc[i - 1];
		x = r;
		a.push_back(v[t]); // 剩余数里的第 t + 1 个数
		v.erase(v.begin() + t); // 移除选做当前位的数
	}
	for (int i = 0; i < a.size(); i++) {
		printf("%d", a[i]);
	}
	return;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值