简介
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。本文内容包括逆康托展开。
分析
康托展开
设全排列的元素总数为 。一个全排列对应的自然数,为此全排列在所有全排列中的字典序。下面以 ,排列为 43512 为例,展示康托展开的公式如何得到。
对于首位 4,有 3 个元素比它小。后面的 4 位共有 种排列。则必有 种排列字典序小于原排列。若首位选择锁定 4 ,与所求排列保持相同,则考虑第二位 3 ,余下共有 2 个元素比它小。则又可得 种合法排列。以此类推,得所求字典序:
加 是因为要算上原排列自身。
推广到所有 ,得到康托展开的公式:
为原排列在第 位之后,有多少个元素小于第 位的元素。
code
朴素算法 。可用树状数组优化至 。
#include <bits/stdc++.h>
#define N 1000010
using namespace std;
typedef long long ll;
const ll mod = 998244353;
template <typename T> inline void read(T &x)
{
x = 0; T f = 1; char ch = getchar();
while (!isdigit(ch)) {if (ch == '-') f = -f; ch = getchar();}
while (isdigit(ch)) {x = x * 10 + ch - 48; ch = getchar();}
x *= f;
}
ll n, a[N], tr[N], ans, fac[N], can[N];
ll lowbit(ll x) {return x & -x;}
void add(ll x, ll val)
{
while (x <= n) {
tr[x] += val;
x += lowbit(x);
}
}
ll query(ll x)
{
ll res = 0;
while (x) {
res += tr[x];
x -= lowbit(x);
}
return res;
}
void pre()
{
fac[1] = fac[0] = 1;
for (int i = 2; i <= n; i++)
fac[i] = fac[i - 1] * i % mod;
for (int i = n; i >= 1; i--) {
can[i] = query(a[i]) * fac[n - i] % mod;
add(a[i], 1);
}
}
int main()
{
read(n);
for (int i = 1; i <= n; i++)
read(a[i]);
pre();
for (int i = 1; i <= n; i++)
ans = (ans + can[i]) % mod;
printf("%lld", ans + 1);
return 0;
}
逆康托展开
逆康托展开是自然数对全排列的映射。下列做一个简单的推导:
于是对 展开得
即
因此在保证 的条件下,可对自然数 分解为
且 的排列唯一。
我们可用类似进制转换的依次作商取余的办法求出所求排列。
代码简单,从略。