P1377 编码II (康拓展开)
空间压缩神器——康拓展开模板!!! 空间压缩神器——康拓展开模板!!! 空间压缩神器——康拓展开模板!!!
题目大意:
输入一个排列(长度小于50),输出它是第几个排列! 第一个排列为1。
分析:
求一个数字不重复的数列在它的全排列中排第几位,这就是康拓展开所求。
如:1 2 3 4 5 在这个数列中排第0位,而1 2 3 5 4排第1位。
但是如果直接全排列排出来一个一个去数,未免太慢太没逼格的了,因此就有了康拓展开的一道标准公式:
康拓=a[n] * (n-1)!+a[n-1](n-2)!+a[n-2](n-3)!+…+a[1]*0!
其中,a[ ]数组表示当前下标数在数列中还未进行展开运算的数字中排名第几小(从0开始算第几小,最小就是第0小),什么意思呢?举个例子就明白了:
例如:2 1 3 4 6 5
从第一个数2开始,我们往后找,发现未扩展得数只有 1 小于2,因此2是属于第1小的一个数,所以a[2]=1,把2进行展开(就是套上面的康拓公式):1*(6-1)!=1*120=120
接下来,未被扩展的数字只剩下了: 1 3 4 6 5
第二个数1,从1开始往后找,没有数小于1,于是1是第0小,a[1]=0,把1进行扩展:0 *(6-2)!= 0 * 24=0
剩余数字:3 4 6 5
第三个数3,往后找没有数小于3,第0小,展开:0 * (6-3)!=0 * 6=0
剩余数字:4 6 5
第四个数4,往后找没有数小于4,展开为:0 * (6-4)!=0 * 2=0
剩余数字:6 5
第五个数6,往后找有1个数小于6,展开:1 *(6-5)!=1 * 1=1
最后:5
第六个数5,往后找已经没有数了,所以是第0小,展开后结果为0
所有的数都展开过了,让后将每位数的展开结果累加为:
120+0+0+0+1=121
因此,2 1 3 4 6 5,在为1 2 3 4 5 6 的全排列中,从0开始,序号是121。也就是说,2 1 3 4 6 5是1 2 3 4 5 6的第121个全排列。
再例如:对于序列3,2,5,4,1
对于3:比3小的有1、2,所以3是第2小的,康拓+=2*(5-1)!
对于2:比2小的有1,所以2是第1小的,康拓+=1*(4-1!)
对于5:比5小的有1、2、3、4,但由于2、3已经出现过了,所以目前5是第2小的,康拓+=2*(3-1)!
对于4:比4小的只剩1,所以 康拓+=1*(2-1)!
对于1:已经是最小的,康拓+=0*(1-1)!
最后获得的答案为60
知道了康拓展开的过程,求排列序号就再也不用死打枚举了——这里模板奉上!!!
康拓展开万能模板: 康拓展开万能模板: 康拓展开万能模板:
为了节省时间,我们先预处理阶乘:
void cal(){
fac[0]=1;
for(int i=1;i<=9;i++) fac[i]=fac[i-1]*i;
}
然后就是康拓展开:
int kangtuo(int* a){
int ans=0;
for(int i=0;i<=n;i++) label[i]=1;
for(int i=0;i<n;i++){
int cnt=0;
for(int j=0;j<a[i];j++) if(label[j]) cnt++;
label[a[i]]=0;
ans+=cnt*fac[n-i-1];
}
return ans;
}
别以为只需要套模板就能过上面那道题,当排列长度巨大,阶乘也会变得巨大,所以得用高精优化的康拓!!
CODE专区
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
char x[1001];
int N=0,num[1001]={0},f[61][101]={0},a[1001]={0},ans[1001]={0};
字符X数组用来存储输入进来的数列,包括空格
NUM数组用来存从字符X中提取出来的数字
F数组用来存阶乘,F[X]表示X的阶乘
A数组用来存和公式一样的:当前数是第几小
ANS数组用来存最后累加后的康拓答案
void mul(int x,int y,int n) // 高精乘
{
int g=0,s=0;
for(int i=100;i>=0;i--)
{
s=f[x][i]*n+g;
f[y][i]=s%10;
g=s/10;
}
}
void add(int x) //高精加
{
int g=0,s=0;
f[0][100]=1;
for(int i=100;i>=0;i--)
{
s=ans[i]+f[x][i]+g;
ans[i]=s%10;
g=s/10;
}
}
void input()
{
gets(x);
for(int i=0;i<strlen(x);i++) //在输入的字符数组中提取数字存入NUM中
{
if(x[i]==' ') continue;
N++;
int j=i;
while(x[j]>='0'&&x[j]<='9')
{
num[N]=num[N]*10+(x[j]-'0'); //数位分离提取数字存入NUM数组中
j++;
}
i=j;
}
f[0][100]=1; //切记!!0!表示1!!!
for(int i=1;i<=N;i++) mul(i-1,i,i); //用高精乘算阶乘
}
void sortt() //求从1到N每个数分别是第几小
{
for(int i=1;i<=N;i++)
{
for(int j=i+1;j<=N;j++)
if(num[j]<num[i])
a[i]++;
}
}
void cangtuo()
{
int t=N-1;
for(int i=1;i<=N;i++) //模拟康拓展开的计算公式
{
mul(t,t,a[i]);
add(t);
t--;
}
int i=0;
while(ans[i]==0&&i<101) i++; //输出
for(int len=i;len<=100;len++)
cout<<ans[len];
}
int main()
{
input();
sortt();
cangtuo();
return 0;
}
此为 A C 代码 此为AC代码 此为AC代码
总结
康拓展开在实际运用的时候一般是不需要用到高精度的(除非题目数据毒瘤),因此代码量不会很大,可以如同高精一样,作为一种工具运用在程序里面。
但,了解了这么多有关康拓展开的东西,它到底有什么值得运用的呢?似乎只能求排列序号而已吧?
有这种想法的人可以看看这道题:八数码
怎样,是不是题目咋一看很简单,其实并不容易:因为,如果要记录下八数码棋盘中的状态,不用任何压缩的话就要耗费很大的内存空间,这对巨佬来说是无法忍受的。但是,通过题目我们不难发现,八数码棋盘的每一种状态中的数字,在当前状态都只会出现一次——这不就是排列吗?
是的,八数码问题的所有状态就是对0 1 2 3 4 5 6 7 8这9个数的全排列
知道了这一点,我们就不必耗费大量的空间存储状态,直接以时间换空间,用一个康拓算法求出当前状态在排列中的编号,然后用来存储——秒变空间压缩大师。
由此可见,哪怕再奇葩的算法和公式,都有他能发挥作用的时候,而对于康拓展开,我认为这绝对是一件空间压缩神器。
当然,如果真的想用康拓算法来进行空间压缩,那么总要提取吧?那么就得用到和康拓展开成对的一个算法——“康拓逆展开”来进行将康拓展开后的数据化为数列。
拓展:
康托逆展开就是知道排名,求出当前数列
首先,把排名转化为以0为开始的排名【就是自减1】
举个例子吧:
对于1,2,3,4,5,求第10的数列
10-1=9
第一个数:9/(5-1)!=0…9,所以第一个数是当前未出现的第0个数:1
第二个数:9/(4-1)!=1…3,所以第二个数是当前未出现的第1个数:3
第三个数:3/(3-1)!=1…1,所以第二个数是当前未出现的第1个数:4
第四个数:1/(2-1)!=1…0,所以第二个数是当前未出现的第1个数:5
第五个数:0/(1-1)!=0…0,所以第二个数是当前未出现的第0个数:2
就这样,第十数列就是1,3,4,5,2
void codel(int x){
int cnt;
for(int i=0;i<m;i++) label[i]=1;
for(int i=0;i<m;i++){
cnt=x/fac[m-1-i];
x=x%fac[m-1-i];
for(int j=0;j<m;j++){
if(!label[j]) continue;
if(!cnt) {label[j]=0;n[i]=j;break;}
cnt--;
}
}
}
为爆肝爱好者准备的康拓展开练习题:
https://www.luogu.com.cn/problem/P5367
var code = "c70bd9d6-9672-4325-883b-6f432dbb2d81"
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!