Cantor展开
这是大概半年前学到的trick,今天突然想起来就来复习一下。
我们知道对于(n)个数(1,2,dots,n)的排列一共有(n!)个,同时我们很容易定义两个不同排列(p_1,p_2)的大小关系——按字典序比较就行,于是我们很自然地会发现,对于任意一个排列(P=(p_1,p_2,dots,p_n)),应该是能够和自然数建立一一对应的关系的——依靠这个排列在所有排列里字典序的排名。
Cantor展开就是做这件事情,我们定义最小的排列(1,2,3,4,5)的排名是0,接着往下(1,2,3,5,4)的排名就是1,Cantor展开的值也就是1。
举个栗子
对于任意一个排列(P)我们也容易算出他的排名:
以(4,3,5,1,2)为例,第一个元素是(4)意味着以(1,2,3)为开头的排列的排名都一定比他小,这样的排列有(3 imes 4!)个,同样,如果定下第一个元素是4,对于前两个元素是((4,3)),意味着((4,1),(4,2))为开头的排列的排名比他小,这样的排列有(2 imes 3!)个,以此类推,定下前两个元素,对于前三个元素是((4,3,5))的排列,意味着以((4,3,1),(4,3,2))为开头的元素排名比他小,这样的排列又有(2 imes 2!)个,接着就是((4,3,5,1))为开头,以((4,3,5))为开头的排列没有比他小的,所以最后比当前排列小的排列的个数就是:(3 imes 4!+2 imes 3!+2 imes 2!=88)个,这也就是这个排列的排名/Cantor展开的值。
实现
容易发现,一个排列(P=(p_1,p_2,dots,p_n))的Cantor展开(egin{aligned}X=sum_{i=1}^n a_i (n-i)!end{aligned}),其中(a_i)表示有多少个(t)使得((p_1,dots,p_{i-1},t)>(p_1,dots,p_i))这个排列比原排列(P)来的大,因为前面用过的已经不能用了,所以这个(t)的个数其实就是在(p_{i+1},dots,p_n)里比(p_i)小的数的个数,这可以用一个树状数组来维护,于是求一个排列Cantor展开的系数就可以做到(O(nlog n))的复杂度啦。
一些事项
Cantor展开其实是一个变进制数,在一些地方似乎能用得上(好像有见过)。
Cantor展开的值虽然很大,不过感觉更多时候是算系数来解决一些问题。
逆过程
都说是一一对应关系,那一个自然数也应该要能映射回去对吧?首先一个具体的数字转换成(n)个系数的表达方式很好做:按照上面的定义,很明显有(a_ileq n-i),对于(t=0,1,dots,k-1),有(k!>t(k-1)!),所以如果我们只有一个(X,n),每次求(a_i)的时候,(a_i=X/(n-i)!),再用(X mod(n-i)!)来更新(X)就行。
接下来就是要怎么通过系数求出具体的排列了,依然用前面的栗子,((4,3,5,1,2))生成的Cantor展开的系数应该是((3,2,2,0,0)),第一个3意味着({1,2,3,4,5})里有3个比第一个元素小的,那么第一个元素就是4,接着我们定下来4,再从({1,2,3,5})里选排名=2+1=3的3,第二个元素就是3,接着往下,从({1,2,5})里选排名第三小的,也就是5,接着是({1,2})里选排名第0+1=1的1…
一个比较简单的实现就还是一个树状数组:值域树状数组,一开始全部设为1,每次二分找到最小的前缀和超过(a_i)的位置,就是这个位置上的答案啦,这样做是(O(nlog ^2 n))的,如果要优化掉这个(log)那就是直接在线段树上二分啦。
CF501D
给两个排列(p,q),(Ord(p))表示一个排列(p)的排名,求排名为((Ord(p)+Ord(q))mod (n!))的排列。
就是一个裸的Cantor展开啦:
rep(i,1,n)modify(i,1);
rep(i,1,n){
f[i]=query(a[i]);
modify(a[i]+1,-1);
}
rep(i,1,n)modify(i,1);
rep(i,1,n){
f[i]+=query(b[i]);
modify(b[i]+1,-1);
}
for(int i=n;i>=1;i--)if(f[i]>n-i){
f[i-1]++;
f[i]%=n-i+1;
}
rep(i,1,n)modify(i,1);
rep(i,1,n){
int l=1,r=n,ret=-1;
while(l<=r){
int mid=(l+r)>>1;
int q=query(mid);
if(q<f[i]+1)l=mid+1;
else{
r=mid-1;
ret=mid;
}
}
modify(ret,-1);
printf("%d ",ret-1);
}