luogu P3014 [USACO11FEB]牛线Cow Line
Description
有N头牛,和K个询问
询问关键字为P时,读入一个数x,输出N的第x个全排列;为Q时,读入N个数,求这是第几个全排列
Hint
康托展开(Cantor expansion)
首先推荐一个大佬的博客
康拓展开用于计算一个N位的排列是所有N的排列中的第几个,它的算式是$$X=a_n*(n-1)!+a_{n-1}*(n-2)!+…+a_i*(i-1)!+…+a_1*0!$$
其中$a_i$表示当前元素在所有未出现的元素中的序号(从0开始),并且$a_i∈[0,i) (1<=i<=n)$
其实很好推理,以9的全排列举例:842697513是1-9全排列的第几个?
首先看第一位为8,那么第一位为1-7的全排列都比它小,共有7*8!个。
在第一位为8的情况下,其次看第二位为4,那么第二位为1-3的全排列都比它小,共有1*3*7!个
在第一位为8,第二位为4的情况下,那么第三位为1的全排列都比它小,共有1*1*6!个
在第一位为8,第二位为4,第三位为2的情况下,那么第四位为1-5的全排列都比它小,这里由于第二位4和第三位2已经确定,第四位的可能取值只能为(1,3,5),共有1*1*1*3*5!
在第一位为8,第二位为4,第三位为2,第四位为6的情况下,那么第五位为1-8的全排列都比它小,这里由于第二位为4,第三位为2,第四位为6已经确定,第五位的可能取值只能为(1,3,5,7),共有1*1*1*1*4*4!
对于第i位的可能取值,前i位的数字已经确定,且在全排列中每一个数字只能出现一次,只需要比较i位后面的数字比i位数字小的有几个,即可知道第i位的可能取值
则推出康拓展开式
逆康拓展开
即求N的全排列中的第N个
就是康拓展开的逆运算
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=m;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); // 移除选做当前位的数
}
}
其实求法和进制转化有异曲同工的感觉呢.
这就是康拓展开逆向考虑应该能理解吧…
code
#include<cstdio>
#include<cstring>
#include<iostream>
#define int long long
using namespace std;
int fac[25]={1};
int n,k,x;
int val[25];
bool vis[25];
void reverse_contor(int x){
memset(vis,0,sizeof vis);
x--;
int j;
for(int i=1;i<=n;i++){
int t=x/fac[n-i];
for(j=1;j<=n;j++){
if(!vis[j]){
if(!t) break;
t--;
}
}
printf("%d ",j);
vis[j]=1;
x%=fac[n-i];
}
puts("");
}
int contor(int x[]){
int p=0;
for(int i=1;i<=n;i++){
int t=0;
for(int j=i+1;j<=n;j++){
if(x[i]>x[j]) t++;
}
p+=t*fac[n-i];
}
return p+1;
}
signed main(){
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i;
while(k--){
char ch;cin>>ch;
if(ch=='P') scanf("%lld",&x),reverse_contor(x);
else{
for(int i=1;i<=n;i++) scanf("%lld",&val[i]);
printf("%lld\n",contor(val));
}
}
return 0;
}