[笔记]康托展开

康托展开是一种将长度为 \(n\) 的排列与 \(n!\) 个整数之间建立双射关系的算法。

前者到后者称为康托展开,后者到前者称为逆康托展开。

康托展开

P5367 【模板】康托展开

对于长度为 \(n\) 的排列 \(a\),其在所有 \(n!\) 个排列中的排名(即字典序从小到大的位次)为:

\[1+\sum\limits_{i=1}^n S_i\times (n-i)! \]

其中,\(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;
}
posted @ 2025-09-03 21:01  Sinktank  阅读(51)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.