康托展开(以及相关例题和代码)

1.定义

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

当然了通俗易懂的来说的话,康托展开就相当于给每个全排列加个序号,每个全排列都有其自己独特的编号,比如说123456的编号为1,123465的编号为2(这时候是不是感觉有一点儿深搜的感觉,就相当于其全排列编号就是其是第几个全排列式子,反正我是这么理解的,个位酌情理解即可)

2.康托展开式

这里我们来讲解一下这里的ai指的都是啥,我们的ai指的是对于全排列中的从右往左的第i个数,有多少个本来该出现在前面的数没有出现,比如说54231这个排列方式,此时的a5应为4,因为5的前面本应该有4个数,但是一个也没有了,因此a5=4,那么5就可以固定了,再看a2,,前面三个数都已经固定了,看后续的,3的前面只有一个1,没有出现过,所以a2=1;,因此我们就可以通过上述公式来计算其康拓表达式的值,但是我们算出来了,还需加上一个1,因为每个全排列的序号最低为1,而也就是按顺序的全排列表,其康托展开式的值为0,12345,其康托展开式的值为0

3.康托展开式代码

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
 
//先把阶乘算出来
int f[20];
void jiecheng(int n)
{
    f[0] = f[1] = 1; // 0的阶乘为1
    for(int i = 2; i <= n; i++) f[i] = f[i - 1] * i;
}
 
//康托展开
int kangtuo()
{
    int ans = 1;  //注意,因为 12345 是算作0开始计算的,最后结果要把12345看作是第一个
    int len = str.length();
    for(int i = 0; i < len; i++)
{
        int tmp = 0;//用来计数的
 
        for(int j = i + 1; j < len; j++)
{
            if(str[i] > str[j]) tmp++;
            //计算str[i]是第几大的数,或者说计算有几个比他小的数
        }
 
        ans += tmp * f[len - i - 1];
    }
    return ans;
}
 
int main()
{
    jiecheng(10);
    string str = "52413";
    cout<<kangtuo()<<endl;
}

4.康托展开式的逆运算

ps:这里由于例子太难写了,所以直接借鉴ltrbless-CSDN博客神牛博客里面的一些例子了

在一开始,我么就已经说过了,康托展开式是全排列和自然数的双射,那么自然是可逆的

这里直接开栗子:

如果初始序列是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<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
 
//算出阶乘
int f[20];
int x, num;
 
void jie_cheng(int n)
{
    f[0] = f[1] = 1; // 0的阶乘为1
    for(int i = 2; i <= n; i++) f[i] = f[i - 1] * i;
}
 
//康托逆展开
 
vector<char> vec; //存需要排列的字符
void rev_kangtuo(int k) //输出序号为 k 的字符序列
{
    int n = vec.size(), len = 0;
    string ans = "";
    k--; // 算的时候是按 12345 是第0位
    for(int i = 1; i <= n; i++){
        int t = k / f[n - i]; // 第 i 位需要 第 t + 1 大的数
        k %= f[n - i];        //剩下的几位需要提供的排列数
        ans += vec[t] ; //  vec[t] 就是第 t + 1 大的数
        vec.erase(vec.begin() + t); 
//用过就删了,不用vector用暴力也可以,就是说枚举,然后一个一个的比较大小,并记录有几个没用过的字符且字典序比它小
    }
    cout << ans << '\n';
}
 
// 假设展开后不超过10位
int main()
{
    jie_cheng(10); // 预处里好阶乘
    scanf("%d", &x); // 输入需要逆展开的数字
    /************康托逆展开***********/
    for(int i = 1; i <= 10; i++)
    {
    	if(x / f[i] == 0) // 求出 x 逆展开所需的最小的位数,方便下面的初始化
    	{
    		num = i;
    		break;
    	}
    }
    for(int i = 1; i <= num; i++) vec.push_back(i + '0'); //输入的位数只要不小于num就可以
    rev_kangtuo(x);
}

5.康托展开的例题

 今天就先列举一个,后续会将另一个例题补上

题解:这题看题就是需要用到bfs这应该是我们脑子里面首先想到的,其次呢,这题有个不一样的地方就是需要用到康托展开,去吧每一种全排列的编号记录下来,然后用ans数组记录其 相应的步骤,然后最后输出就好了(这里要注意的的是,你输入的12345678,但其实是12348765),因此我们需要在后面给他转换一下

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include<string>
#include <queue>
using namespace std;
string start,end1;//初始的目标状态,和改变后的目标状态
string step[1000000];//用于记录整个流程的操作
int vis[1000000];//判断是否已经到达这个状态
int num[10]={1},pos[10];//num数组用于计算阶乘,pos数组用于装start中的每个数字在其start中的位置

struct node
{
	string step;//用于记录执行的操作
	string str;//目标字符串
	int hash;//对应的康托展开值
};

int ni(string &s)//求逆序数
{
	int sum=0;
	for(int i=0;i<7;i++)
	{
		int cnt=0;
		for(int j=i+1;j<8;j++)
		{
			if(s[i]>s[j])
			cnt++;
		}
		sum+=cnt*num[7-i];
	}
	return sum;
}

//执行三种操作
void doa(string &s)
{
	for(int i=0;i<4;i++)
	{
		swap(s[i],s[i+4]);
	}
}

void dob(string &s)
{
	char tmp=s[3];
	for(int i=2;i>=0;i--)
	{
		s[i+1]=s[i];
	}
	s[0]=tmp;
	tmp=s[7];
	for(int i=6;i>=4;i--)
	{
		s[i+1]=s[i];
	}
	s[4]=tmp;
}

void doc(string &s)
{
	char tmp=s[1];
	s[1]=s[5];
	s[5]=s[6];
	s[6]=s[2];
	s[2]=tmp;
}

//广搜执行三种操作,找到每一种状态的操作放方式
void bfs()
{
	memset(vis,0,sizeof(vis));
	node now,next;
	queue<node>q;
	now.step = "";
    now.str = start;
    now.hash = ni(start);
    q.push(now);
    vis[ni(start)]=1;
    step[ni(start)]="";
	while(!q.empty())
	{
		now=q.front();
		q.pop();
		string t;
		int cnt;
		
		t=now.str;
		doa(t);
		cnt=ni(t);
		while(vis[cnt]==0)
		{
			vis[cnt]=1;
			next=now;
			next.step+='A';
			step[cnt]=next.step;
			next.str=t;
			next.hash=cnt;
			q.push(next);
		}
		
		t=now.str;
		dob(t);
		cnt=ni(t);
		while(vis[cnt]==0)
		{
			vis[cnt]=1;
			next=now;
			next.step+='B';
			step[cnt]=next.step;
			next.str=t;
			next.hash=cnt;
			q.push(next);
		}
		
		t=now.str;
		doc(t);
		cnt=ni(t);
		while(vis[cnt]==0)
		{
			vis[cnt]=1;
			next=now;
			next.step+='C';
			step[cnt]=next.step;
			next.str=t;
			next.hash=cnt;
			q.push(next);
		}
	}
}
int main()
{
	for(int i=1;i<10;i++)
	{
		num[i]=num[i-1]*i;
	}
	start="12345678";
	bfs();
	while(cin>>start>>end1)
	{
		swap(start[4], start[7]);
        swap(start[6], start[5]);
        swap(end1[4], end1[7]);
        swap(end1[6], end1[5]);
        for (int i = 0; i < 8; i++)
        {
        	pos[start[i]-'0'] = i + 1;
		}
        for (int i = 0; i < 8; i++)
        {
        	end1[i] = pos[end1[i]-'0'];//存储每个数在start数组的位置 
		}
        int cnt;
        cnt=ni(end1);
        cout << step[cnt] << endl;
        
	}
	return 0;
}

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值