康托展开
问题解决:求一个n位的全排列按字典序升序排序的位置
例如一个n位排列 2 3 4 1,我们用rank来代表他的排位
从第一位开始:比2小的且没出现在之前的数有: 1 ,那么他对答案带来的贡献自然是 3!
rank += 3! rank = 6;
第二位(注意,这时第一位已经确定是2了):比3小的且没出现在之前的数有: 1. =>2!
rank += 2! rank = 8;
第三位:比4小的且没出现在之前的数为 1. => 1!
rank += 1! rank = 9;
最后一位不用计算,因为前面n-1位已经确定了以后最后一位就唯一确定了。
这时候,rank的含义还是,这个排列之前有多少个排列 . rank + 1之后才是他实际的位置.
推出普适的式子:
a[i]代表第i位之后小于第i位的数的个数.
请注意:从右到左,i从小到大递增.
逆康托展开
我们可以看出,一个排列和一个整数是一一映射的,又称双射。也就是说,一个排列能唯一确定一个整数,那么显然一个整数也一定能唯一确定一个排列。也就是说,一定有一种方法使一个整数转化为一个排列!这种方法就叫做逆康托展开
例如: 已知2 3 4 1 的rank = 9,我们现在尝试用9还原这个排列。
9/3! = 1 ... 3 代表第一位有一个数小于原数, 显然这个数就是2;第一位的数确定了,取余数继续运算.
3/2! = 1 ... 1 代表第二位有一个数小于原数(同时,这个数也不能在之前出现过哦!),显然这个数就是3,
1/1! = 1 ... 0 .代表第三个数有一个数小于原数,显然这个数就是4.
第四位唯一确定,不用计算.
代码实现:
暴力 n^2 很容易实现,n过大时我们考虑引入高级数据结构进行优化
康托展开:实质上就是维护一个树状数组就OK
逆康托展开:维护一个单点修改的权值线段树,每次求第k大就好。也可以树状数组+二分.O(nlognlogn)
注:由于阶乘很大,所以逆康托展开不会太大 20! ≈ 2e18
具体应用:
主要的应用就是将一个排列压缩成一个整数储存。可以应用于BFS求八数码问题里
模板:
暴力康托展开:
int str[105];
int fact[105];
void init_fact(int n)
{
fact[0] = 1;
for (int i = 1 ; i <= n ; i++) fact[i] = fact[i - 1] * i;
}
int Contor(int str[],int n) // str从1开始储存
{
int ans = 0;
for(int i = 1; i <= n; i++)
{
int cnt = 0;
for(int j = i+1; j <= n; j++)
{
if(str[i] > str[j])
cnt++;
}
ans += cnt*fact[n-i];
}
return ans + 1;
}
暴力逆康托展开:
void deContor(int str[], int n, int x) //第x大数字序列(从0开始),1-n排列.str从1开始存储
{
int i, j;
int cnt;
bool visited[100];
memset(visited , 0 , sizeof visited);
for(i = n-1; i >= 0; i--)
{
int consult = x/fact[i]; //每一项的系数
x -= consult*fact[i]; //余数
for(j = 0, cnt = 0; cnt <= consult; j++)
{
if(!visited[j+1]) //未被使用的数字中,第consult大的数字
cnt++;
}
visited[j] = true; //标记第consult大的已经被使用
str[n - i] = j;
}
}