一、引入
以前从来没听过“康托展开”的 myt 在做「NOIP2018 提高组」初赛卷的时候,碰到了一道需要用到康托展开的题,于是就有了以下内容。
那么康托展开是什么呢?康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。(摘自百度百科)
通俗地说,康托展开可以用来求一个 (1sim n) 的任意排列的排名(把 (1sim n) 的所有排列按字典序排序,这个排列的位次就是它的排名)。
康托展开可以在 (O(n^2)) 的复杂度内求出一个排列的排名,在用到树状数组优化时可以做到 (O(nlog n))。
二、康托展开
1. 公式
先给出康托展开的公式:
(X=a_1(n−1)!+a_2(n−2)!+cdots+a_ncdot 0!)
其中,(a_i) 为整数,并且 (0leq a_i<i,1leq ileq n)。
(X) 表示康托展开的结果。(a_i) 表示在当前未出现的元素中比第 (i) 个数小的数的个数,后面乘的是还剩下的数的个数的阶乘。
2. 例子
举个栗子:求长为 (5) 的排列 ([5,2,4,1,3]) 的排名。
1. 首先,若第一个数已经确定,那么还有 (4) 个数没有确定,则共有 (4!) 种排列。而小于 (5) 的数开头的排列都会比 (5) 开头的排列的字典序要小。令 (1,2,3,4) 分别作为第一个数,也就是说第一个数有 (4) 种选择,所以共有 (4 imes 4!) 种排列。这 (4 imes 4!) 种排列都比 ([5,2,4,1,3]) 的字典序小。
2. 再来看第二位。由于第一位 (5) 已经出现过了,所以还剩下 ({1,2,3,4})。第一位 (5) 已经确定,接下来小于 (2) 的数作为第二位的排列都会比 (2) 作为第二位的排列的字典序要小(第一位都是 (5))。({1,2,3,4}) 中只有 (1) 个数比 (2) 小。与上一步同理,这时候又有 (1 imes 3!) 种排列比 ([5,2,4,1,3]) 的字典序小。
3. 然后看第三位。还剩下 ({1,3,4}),其中比 (4) 小的有 (2) 个。那么有 (2 imes 2!) 种排列比 ([5,2,4,1,3]) 的字典序小。
4. 接下来看第四位。还剩下 ({1,3}),其中比 (1) 小的有 (0) 个。则有 (0 imes 1!) 种排列比 ([5,2,4,1,3]) 的字典序小。
5. 最后再看第五位。此时有 (0 imes 0!) 种排列比 ([5,2,4,1,3]) 的字典序小。
(4!) | (3!) | (2!) | (1!) | (0!) |
---|---|---|---|---|
(24) | (6) | (2) | (1) | (0) |
综上所述,共有 (4 imes 4!+1 imes 3!+2 imes 2!+0 imes 1!+0 imes 0!=106) 种排列比 ([5,2,4,1,3]) 的字典序小。则 ([5,2,4,1,3]) 的排名为 (107)。
int solve(){ int ans=0; for(int i=1;i<=n;i++){ int cnt=0; //在当前未出现的元素中比第 i 个数小的数的个数 for(int j=i+1;j<=n;j++) //当前未出现的元素:a[(i+1)~n] if(a[j]<a[i]) cnt++; ans+=cnt*f[n-i]; //f[i]=i! } return ans+1; //当前算出的是,比给出排列字典序小的排列数,所以还要 +1 }
三、逆康托展开
逆康托展开可以计算这样的问题:给出一个 (1sim n) 的排列的排名,求这个排列。
举个栗子:已知一个 (1sim 5) 的排列 (A) 的排名为 (107),求 (A)。
首先,(A) 的排名为 (107),那么共有 (106) 种排列比 (A) 的字典序小。
1. (lfloor frac{106}{4!} floor=4),则有 (4) 个数比 (A) 的第一位小,则第一位为 (5)。
2. 还剩下 (106-4 imes 4!=10) 种排列比这个排列的字典序小((frac{106}{4!}=4 cdotscdots 10))。由于第一位 (5) 已经出现过了,所以还剩下 ({1,2,3,4})。(lfloor frac{10}{3!} floor=1),则在 ({1,2,3,4}) 中有 (1) 个数比 (A) 的第二位小。则第二位为 (2)。
3. (10-1 imes 3!=4)。(lfloorfrac{4}{2!} floor=2),则在 ({1,3,4}) 中有 (2) 个数比 (A) 的第三位小。则第三位为 (4)。
4. (4-2 imes 2!=0)。(lfloorfrac{0}{1!} floor=0),则在 ({1,3}) 中有 (0) 个数比 (A) 的第四位小。则第三位为 (1)。
5. (0-0 imes 1!=0)。(lfloorfrac{0}{0!} floor=0),可得第三位为 (3)。
所以 (A=[5,2,4,1,3])。
void solve(int n,int k){ //已知一个 1~n 的排列 A 的排名为 k,求 A memset(vis,0,sizeof(vis)),k--; //有 k-1 种排列比 A 的字典序小 for(int i=1;i<=n;i++){ int cnt=k/f[n-i]; //有 cnt 个数比 A 的第 i 位小 for(int j=1;j<=n;j++) if(!vis[j]){ if(!cnt){a[i]=j,vis[j]=1;break;} cnt--; } k%=f[n-i]; //还剩下几种排列比 A 的字典序小 } }
四、树状数组优化
1. 树状数组优化康托展开
思考康托展开的过程。先不管乘上阶乘的部分,考虑优化求当前未出现的元素中比第 (i) 个数小的数的个数(当前有多少个小于它的数还没有出现)的做法。
考虑用树状数组维护。具体来说,初始时我们把树状数组中的每个数设为 (1),表示这个数还没有出现。如果某个数出现过了,就把它变成 (0)。最后查询 (1sim a_{i-1}) 有多少个 (1) 就行了(因为比它小的数都在它的前面,并且出现过的数的值都为 (0))。
#include<bits/stdc++.h> #define int long long #define lowbit(x) x&(-x) using namespace std; const int N=1e6+5,mod=998244353; int n,a[N],f[N],c[N],ans; void modify(int x,int y){ for(int i=x;i<=n;i+=lowbit(i)) c[i]=(c[i]+y)%mod; } int query(int x){ int ans=0; for(int i=x;i;i-=lowbit(i)) ans=(ans+c[i])%mod; return ans; } signed main(){ scanf("%lld",&n),f[0]=1; for(int i=1;i<=n;i++) f[i]=(f[i-1]*i)%mod; for(int i=1;i<=n;i++) scanf("%lld",&a[i]),modify(a[i],1); //初始时每个数都设为 1,表示这个数还没有出现 for(int i=1;i<=n;i++) modify(a[i],-1),ans=(ans+query(a[i]-1)*f[n-i]%mod)%mod; //如果某个数出现过了,就把它变成 0。查询 1~a[i-1] 中 1 的个数。 printf("%lld ",ans+1); return 0; }
2. 树状数组+二分优化逆康托展开
把在未选数中找到第 cnt+1 大的数的过程改成二分+树状数组即可。
#include<bits/stdc++.h> #define int long long #define lowbit(x) x&(-x) using namespace std; const int N=1e6+5; int n,k,a[N],f[N],c[N]; void modify(int x,int y){ for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y; } int query(int x){ int ans=0; for(int i=x;i;i-=lowbit(i)) ans+=c[i]; return ans; } void solve(int n,int k){ k--; for(int i=1;i<=n;i++) modify(i,1); //初始时每个数都设为 1,表示这个数还没有出现 for(int i=1;i<=n;i++){ int cnt=k/f[n-i],l=1,r=n,ans; while(l<=r){ //二分求,未出现过的数中,有 cnt 个数比它小的数 int mid=(l+r)/2; if(query(mid)-1>=cnt) ans=mid,r=mid-1; //query(mid)-1 即查询 mid 前面有几个数还没有出现。这里若用 query(mid-1) 会出锅,因为可能会取到之前出现过的数。 else l=mid+1; } a[i]=ans,k%=f[n-i],modify(a[i],-1); //如果某个数出现过了,就把它变成 0 } } signed main(){ scanf("%lld%lld",&n,&k),f[0]=1; for(int i=1;i<=n;i++) f[i]=f[i-1]*i; solve(n,k); for(int i=1;i<=n;i++) printf("%lld%c",a[i],i==n?' ':' '); return 0; }