康托展开
一些定义
有一个长度为 \(n\) 的排列 \(P\)。
定义排列通过字典序比大小,即若长度为 \(n\) 的排列 \(A,B\) 有
- 任意 \(1\le j<i\le n,A_j=B_j\);
- \(A_i<B_i\),
则有排列 \(A<B\)。这里不考虑长度不一样的情况。
定义一个排列 \(P\) 的排名 \(rnk(P)\) 为比它小的排列数量 \(+1\)。
康托展开
给定 \(P\),求 \(rnk(P)\)。
等价于:求长度为 \(n\) 的排列 \(Q\),满足 \(Q<P\) 的数量(\(+1\))。
考虑枚举定义中的 \(i\)。于是 \(\forall 1\le j<i\le n,Q_j=P_j\)。那么 \(Q_i\) 可以是哪些数呢?
- \(Q_i<P_i\implies Q_i\ne P_i\implies i\ne k\)。
- \(Q_i\ne Q_j\iff \exist k,i\le k,Q_i=P_k\)。
\(\implies\exist k,i<k,Q_i=P_k<P_i\),故 \(Q_i\) 的取值数量(记为 \(f(i)\))即为
其中 \([X]\) 表示条件 \(X\) 是真(\(1\))/假(\(0\))。
这是典型的二维偏序问题。可以用树状数组实现。
求出了 \(Q_i\) 取值数量,考虑求 \(Q_{i+1}\sim Q_n\) 的排列数量。
由于已经满足 \(Q_i<P_i\),故后面 \((n-i)\) 个数可以随便排列,排列数量为 \((n-i)!\)。故在 \(i\) 处小于 \(P\) 的排列数量为
将所有 \(i\) 的贡献相加,即可得到
倒序枚举 \(i\),\(f(i)\) 可以使用树状数组在 \(O(\log n)\) 时间计算,故该算法可以在 \(O(n\log n)\) 时间内计算某个排列的排名,被称为康托展开。
逆康托展开
排名可以逆推得到排列。
但可以发现有 \(n=20\) 时,全排列数量 \(n!=2432902008176640000>10^{18}\)。
所以,若题目给的排名 \(rnk(P)\le 10^{18}\)(不需要高精),有 \(n=|P|\le 20\)。
于是,我们可以完成如下 \(n\) 次操作:
- 设 \(r_0=rnk(P)-1\),集合 \(S=\{1,2,\cdots,n\}\)。
- 对于第 \(j\) 次操作,\(r_j\gets r_{j-1}\bmod (n-j)!\),\(k_j\gets\cfrac{r_{j-1}-r_j}{(n-j)!}\)。
- 从 \(S\) 中取出第 \((k_j+1)\) 大的元素 \(x\),有 \(P_j=x\)。从 \(S\) 中删除 \(x\)。
- 重复进行上面两步 \(n\) 次。
解释一下:
每次操作从 \(r_{j-1}\) 中分离出 \(f(j)\),以 \(j=1\) 为例
有没有发现 \(r_1\) 与 \(r_0\) 几乎一致(除了 sigma 下界从 \(1\) 变成 \(2\))?
对于 \(k_1\),
如此这般,我们进行了完美的迭代,每次将 \(f(j)\) 分离,从 \(S\) 中找出合适的 \(x\)。然后将 \(r_{j-1}\) 中 \(i=j\) 那项消去(simga 上界加 \(1\))。
形式化地说,我们的处理方式使得有 \(r_j,k_j\) 的通项公式
\(S\) 可以用 vector
维护,复杂度 \(O(n^2)\)。因为 \(n\le 20\),复杂度足够。
若毒瘤出题人让 \(rnk(P)>10^{18}\),也不要紧。上线段树二分维护 \(S\) 即可。