算法讲解:康托展开与逆康托展开原理与代码实现

康托展开与逆康托展开

本文章是我看过以下两篇博客,思考后所写,在此感谢这两篇文章作者。
文章部分引用了他们的原话。

连接:https://blog.csdn.net/ajaxlt/article/details/86544074
连接:https://blog.csdn.net/ltrbless/article/details/87696372

序言:

本文章会介绍康托展开与逆康托展开的原理与代码实现

1.概念

康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。

意思就是给你n位数的全排列,你可以用康托展开求数是字典序第几位全排列。康托展开计算就是求某个全排列方式在字典序集合中的位置(排名)所以映射关系唯一并且单调,故是可逆的,即给定全排列所有字符,和某个字典序号,我们就可以用逆康托展开求的相对应的全排列。

公式:

后面我讲一个例子大家应该就会理解这个公式是什么意思了。

举例:

设一个数组是由1,2,3,4,组成,那么该数组构成字典序最小的数组[1,2,3,4],该数组在康托展开里编号是0(为什么是0后面会讲),依次[1,2,4,3]的编号是1,[1,3,2,4]编号是2…依次类推。

2.康托展开

拿52413举例子:

(1)、首先看第一个数 5,不管第一位是什么数,后面都有四位数,那么这四位数全排列的方式有 4!种,所以可以令1,2,3,4分别作为开头,这样的话就会有 4 * 4!种排法要比 52413这种排法的字典序要小。

那么第一个数是1,2,3,4时候的字典序的个数数完了是 4 * 4! 种,且这些字典序都要比52413的字典序要小。

(2)、那么就可以固定第一位5,找下一位2,这时5已经用过了,所以从剩下的 1,2,3,4 里挑选比2小的数,一共1个,后面还剩三位,也就是3!种排列方式,那么这时候比 52413 字典序要小的又有 1 * 3!种,也就是当5在第一位,1在第二位的时候。

(3)、再看第三位4,这时5,2都用了,所以从剩下的 1,3,4三个数中找比4小的数的个数,有两个比4小原理同上,所以这时候也可以有 2 * 2!种排列方式的字典序小于 52413

(4)、再看第四位1,这时候会有 0 * 1!种

(5)、再看第五位3,这时候会有0 * 0!种
综上所述:
对于序列: 52413 该序列展开后为: 4 * 4! + 1 * 3! + 2 * 2! + 0 * 1! + 0 * 0! ,计算结果是: 106
由于是从0开始计数的,所以最后 52413 的编号为 107

为什么从0开始计数?
可以这样看:我现在让你求12345的康托展开值,也就是:0* 4!+ 0* 3!+ 0* 2!+ 0* 1!+0* 0! = 0
所以康托公式最小字典序的编号就是0。
康托展开代码

#include<iostream>
#include<unordered_set>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<math.h>
#include<vector>
#include<stack>
#include<queue>
#include<map>
#include<set>
#include<bitset>
#define ll long long
using namespace std;
string str;
int f[20];
void jie_cheng(int n)
{
	f[0]=f[1]=1;
	for(int i=2;i<=n;i++)  f[i]=f[i-1]*i;
}
int kangtuo()
{
	int ans=1;
	int len=str.size();
	for(int i=0;i<len;i++)
	{
		int tmp=0;//用来计数
		for(int j=i+1;j<len;j++) 
			if(str[i]>str[j]) tmp++;
		ans+=tmp*f[len-i-1];
		//cout<<tmp<<" "<<ans<<endl;
	}
	return ans;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
    //freopen("test.in","r",stdin);
    //freopen("test.out","w",stdout);
	jie_cheng(10);
	//for(int i=1;i<=10;i++) cout<<f[i]<<" ";
	//cout<<endl;
	str="52413";
	cout<<kangtuo()<<endl;
	return 0;
}

3.逆康托展开
这里直接开栗子:

如果初始序列是12345(第一个),让你求第107个序列是什么。(按字典序递增)

这样计算:

先把107减1,因为康托展开里的初始序列编号为0
然后计算下后缀积:
1 2 3 4 5
5! 4! 3! 2!1! 0!
120 24 6 2 1 1

106 / 4! = 4 ······ 10 有4个比它小的所以因该是5 从(1,2,3,4,5)里选
10 / 3! = 1 ······ 4 有1个比它小的所以因该是2 从(1, 2, 3, 4)里选
4 / 2! = 2 ······ 0 有2个比它小的所以因该是4 从(1, 3, 4)里选
0 / 1! = 0 ······ 0 有0个比它小的所以因该是1 从(1,3)里选
0 / 0! = 0 ······ 0 有0个比它小的所以因该是3 从(3)里选

所以编号107的是 52413
康托逆展开代码

#include<iostream>
#include<unordered_set>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<math.h>
#include<vector>
#include<stack>
#include<queue>
#include<map>
#include<set>
#include<bitset>
#define ll long long
using namespace std;
int x,num;
int f[20];
vector<char> vec;
void jie_cheng(int n)
{
	f[0]=f[1]=1;
	for(int i=2;i<=n;i++) f[i]=f[i-1]*i;
}
void rev_kangtuo(int k)
{
	int n=vec.size(),len=0;
	string ans="";
	k--;
	for(int i=1;i<=n;i++)
	{
		int t=k/f[n-i];
		k%=f[n-i];
		ans+=vec[t];
		vec.erase(vec.begin()+t);
	}
	cout<<ans<<endl;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
    //freopen("test.in","r",stdin);
    //freopen("test.out","w",stdout);
	jie_cheng(10);
	scanf("%d",&x);
	for(int i=1;i<=10;i++)
	{
		if(x/f[i]==0)
		{
			num=i;
			break;
		}
	}
	for(int i=1;i<=num;i++) vec.push_back(i+'0');
	rev_kangtuo(x);
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值