康托展开总结

康托展开

一、康托展开

1. 定义

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

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

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

2. 原理

设有排列 p=a1a2...anp = a_1 a_2 ... a_np=a1a2...an ,则对于字典序比 ppp 小的排列一定存在排列 p1p_1p1 ,使得排列的前 i−1i - 1i1 位与 ppp 相同,第 iii 位比 pip_ipi 小,后续位随意;

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

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

关于求 AiA_iAi ,可以用 O(n2)O(n^2)O(n2) 求,也可以用树状数组优化到 O(nlogn)O(nlog_n)O(nlogn)

3. 例子

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

第一位比 p1p_1p1 小的排列数为 3∗3!=183 * 3! = 1833!=18

第一位与 p1p_1p1 相等,第二位比 p2p_2p2 小的排列数为 000

第一,二位分别与 p1,p2p_1, p_2p1,p2 相等,第三位比 p3p_3p3 小的排列数为 1∗1!=11 * 1! = 111!=1

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

4. 代码

预处理

预处理出阶乘与 AAA 数组

O(n2)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=1n−1(i∗i!)+1n! = \Sigma_{i = 1}^{n - 1}(i * i!) + 1n!=Σi=1n1(ii!)+1

n!>Σi=1n−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)! 都比后面所有项的总和还大,则可以用类似进制转化的方法,不断除模得到得到 AAA 数组;

得到 AAA 数组后,即可得到 pip_ipi 就是剩余未用的数中第 Ai+1A_i + 1Ai+1 小的;

3. 例子

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

首先 96−1=9596 - 1 = 95961=95

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

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

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

1/1!=11 / 1! = 11/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、付费专栏及课程。

余额充值