[笔记]康托展开
康托展开是一种将长度为 \(n\) 的排列与 \(n!\) 个整数之间建立双射关系的算法。
前者到后者称为康托展开,后者到前者称为逆康托展开。
康托展开
对于长度为 \(n\) 的排列 \(a\),其在所有 \(n!\) 个排列中的排名(即字典序从小到大的位次)为:
其中,\(S_i=\sum\limits_{j=i}^n[a_j<a_i]\)。
理解起来不难,我们仅需求出所有字典序小于 \(a\) 的排列 \(b\) 的个数,再 \(+1\) 就是 \(a\) 的排名了。
我们枚举 \(i\),钦定 \(a,b\) 长度为 \(i-1\) 的前缀均相等,且 \(b_i<a_i\)。而由于这样的 \(b_i\) 只能从 \(i\) 之后取,所以其个数正是 \(S_i\),而 \(i\) 之后的 \(n-i\) 个元素可以随意排列,故为 \((n-i)!\)。
\(S_i\) 可以用树状数组或线段树来维护,下面的代码使用前者。
时间复杂度 \(O(n\log n)\)。
点击查看代码 - R234178546
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10,P=998244353;
int n,m,a[N];
ll fac[N],ans=1;
inline int lb(int x){return x&-x;}
struct BIT{
int s[N];
inline void chp(int x,int v){for(;x<=n;x+=lb(x)) s[x]+=v;}
inline int qry(int x){int a=0;for(;x;x-=lb(x)) a+=s[x];return a;}
}bit;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;
fac[0]=1;
for(int i=1;i<=n;i++) cin>>a[i],fac[i]=fac[i-1]*i%P;
for(int i=n;i>=1;i--){
ans+=bit.qry(a[i]-1)*fac[n-i]%P;
bit.chp(a[i],1);
}
cout<<ans%P<<"\n";
return 0;
}
逆康托展开
对于长度为 \(n\) 的排列,给定排名 \(x\),如何求第 \(x\) 个排列?
仍然转化为构造一个 \(a\),使得小于 \(a\) 的排列数量为 \(x-1\)。
初始状态令 \(B=\{1,2,\dots,n\}\),\(t=x-1\),编号从 \(0\) 开始。
第 \(1\) 位的每种选法,都对应第 \(2\) 位的 \((n-1)!\) 种选法。所以这一位填 \(B[t\bmod (n-1)!]\),并将其从 \(B\) 中删除,\(t\leftarrow t\bmod (n-1)!\)。
第 \(2\) 位的每种选法,都对应第 \(3\) 位的 \((n-2)!\) 种选法。所以这一位填 \(B[t\bmod (n-2)!]\),并将其从 \(B\) 中删除,\(t\leftarrow t\bmod (n-2)!\)。
以此类推即可完成填写。
朴素算法的删除是 \(O(n)\) 的,总时间复杂度为 \(O(n^2)\);可以用平衡树 / 线段树上二分 / 树状数组上二分做到 \(O(n\log n)\)。
可以查看 P1088 [NOIP 2004 普及组] 火星人,但是如果这道题按康托展开求出排名再逆康托回去,排名会非常大,难以存储。
可以参照 yummy 的题解,其中用“变进制数”来存储排名,其实就是将 \(t\) 每次取模的结果记录下来而已。
下面给出朴素算法和线段树优化的代码,均使用这种方法来存储。
朴素 $O(n^2)$ - R234192346
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int n,m,a[N];
bitset<N> vis;//是否从 S 中删除
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int x,i=1;i<=n;i++){
cin>>a[i],x=0;//x 最终表示 a[i] 在 S 中的第几位
for(int j=1;j<=a[i];j++) x+=(vis[j]^1);//即找位置 a[i] 是第几个 0
vis[a[i]]=1;
a[i]=x-1;
}
a[n]+=m;
for(int i=n;i;i--){
a[i-1]+=a[i]/(n-i+1);
a[i]%=n-i+1;
}
vis=0;
for(int i=1,j,x;i<=n;i++){
a[i]++;
for(j=1,x=0;x<a[i];j++) x+=(vis[j]^1);//即找第 a[i] 个 0 在哪个位置
cout<<j-1<<" ";
vis[j-1]=1;
}
return 0;
}
线段树上二分 $O(n\log n)$ - R234195423
#include<bits/stdc++.h>
#define lc (x<<1)
#define rc (x<<1|1)
using namespace std;
const int N=1e4+10;
int n,m,a[N];
struct SEG{
int s[N<<2];
inline void clr(){memset(s,0,sizeof s);}
inline void upd(int x){s[x]=s[lc]+s[rc];}
inline void sep(int x,int a,int v,int l,int r){
if(l==r) return s[x]=v,void();
int mid=(l+r)>>1;
if(a<=mid) sep(lc,a,v,l,mid);
else sep(rc,a,v,mid+1,r);
upd(x);
}
inline int qry(int x,int a,int b,int l,int r){
if(a<=l&&r<=b) return s[x];
int mid=(l+r)>>1,ans=0;
if(a<=mid) ans+=qry(lc,a,b,l,mid);
if(b>mid) ans+=qry(rc,a,b,mid+1,r);
return ans;
}
inline int kth(int x,int k,int l,int r){//第 k 个 0
if(l==r) return l;
int mid=(l+r)>>1,z=mid-l+1-s[lc];
if(k<=z) return kth(lc,k,l,mid);
return kth(rc,k-z,mid+1,r);
}
}seg;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int x,i=1;i<=n;i++){
cin>>a[i],x=a[i]-seg.qry(1,1,a[i],1,n);//x 表示 a[i] 在 S 中的第几位,即找位置 a[i] 是第几个 0
seg.sep(1,a[i],1,1,n);
a[i]=x-1;
}
a[n]+=m;
for(int i=n;i;i--){
a[i-1]+=a[i]/(n-i+1);
a[i]%=n-i+1;
}
seg.clr();
for(int i=1,j;i<=n;i++){
a[i]++;
j=seg.kth(1,a[i],1,n);//即找第 a[i] 个 0 在哪个位置
cout<<j<<" ";
seg.sep(1,j,1,1,n);
}
return 0;
}
浙公网安备 33010602011771号