这篇文章上次修改于 218 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
算法学习笔记(10): 康托展开
可以求一个组合的全排列的名次
比如:对于序列1,2,3
则序列排名为 1 2 3;1 3 2; 2 1 3; 2 3 1; 3 1 2; 3 2 1
这个排列是按字典序排名的。
故依据康托展开,它是一个全排列到自然数的双射,也就是排列映射到排名,排名映射到排列,后者又称作康托逆展开。
求排名过程
对于排列 2 5 3 4 1,这个排列绝对大于以1开头的排列,有4!种。
对于第二位5,已经锁定了第一位为2,所以剩下四个数,要第二位更小,有3x3!种更小的排列。
以此类推有 4! + 3 x 3! + 2! + 1= 45 ,有45个比它小的,所以它是46名。
const int factorial[]={1,1,2,6,24,120,720,5040,40320,362880,3628800};//阶乘0-10
int cantor(int a[],int n){
//cantor展开,n表示是n位的全排列,a[]表示全排列的数(用数组表示)
int ans = 0,sum = 0;
for(int i = 1;i < n;i++){
for(int j = i + 1;j <= n;j++)
if(a[j] < a[i])
sum++;
ans += sum * factorial[n - i];//累积
sum = 0;//计数器归零
}
return ans + 1;
}
求排列过程
对于名次46,要找到排列 2 5 3 4 1
已知排列公式为
又因为 n!严格大于 1~n-1的阶乘和
所以整除向下取整即可求得位的值
static const int FAC[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880}; // 阶乘
//康托展开逆运算
void decantor(int x, int n)
{
vector<int> v; // 存放当前可选数
vector<int> a; // 所求排列组合
for(int i=1;i<=n;i++)
v.push_back(i);
for(int i=n;i>=1;i--)
{
int r = x % FAC[i-1];
int t = x / FAC[i-1];
x = r;
sort(v.begin(),v.end());// 从小到大排序
a.push_back(v[t]); // 剩余数里第t+1个数为当前位
v.erase(v.begin()+t); // 移除选做当前位的数
}
}
时间优化
使用树状数组可以加快计算逆序对的数量。复杂度从 O(N2) → O(NlogN)
八数码问题
八数码问题是指一个移动格子的小游戏,对于一个3x3的区域,有1~8编号的格子,它们只能相邻移动到空位,问最少几次可以排列完成。
显然你可以利用BFS,暴力搜索所有选择,因为走一步变成一个新的状态。然而对于一个BFS,它没有减枝,没有去除重复计算的部分,需要考虑重复计算,就需要保存所有状态的一个visited数组,然而如何对一个状态进行编码。这就用到了康托展开。
康托展开在此处被作为一个哈希函数,把八数码问题的不同排列状态哈希成一个数,这就可以用于处理。