一种玄学的hash排列的方式
康托展开学习笔记
参考资料
何为康托展开
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。 ——百度百科
实际上,康托展开就是给出一个全排列,求它是第几个全排列。
而逆康托展开就是反过来,给出全排列长度和全排列排名,求这个全排列的每一个元素。
实际上,康托展开除了在全排列方面有作用之外,还可以被用来当做一种数列hash,把一整个数列hash成一个值,这样可以大大的节省空间。
如何康托展开
暴力康托展开
我们对于一个全排列,第i
位数有n+1-i
种选择,如果用变进制数表示,这一位的进制就是n+1-i
。
换句话说,如果第i
位选择了第k
种情况,那么对应的变进制数就是k
。
这里我们为了方便,以0为起点做下标。
举个例子,排列:1 4 5 3 2
转换成变进制数就是unknown-(02210)
.
- 1是排列
1 2 3 4 5
中的第一项,变进制数为0. - 4是排列
2 3 4 5
中的第三项,变进制数为2. - 5是排列
2 3 5
中的第三项,变进制数为2. - 3是排列
2 3
中的第二项,变进制数为1. - 2是排列
2
中的第一项,变进制数为0.
可以发现,变进制数的第i
位的值就是$a_i - \sum_{j=0}^{i-1} [a_j<a_i] +1$.
上面这句话的意思是变进制数的第i
位的值就是这个数减去它左边比它小的数再+1.
其实这就是康托展开的定义式。
完整的式子:
$$ans = 1 + \sum_{i=1}^n (\sum_{j=i}^n [a_i < a_j]) \times (n - i)!$$
然后我们暴力做就可以了,用一个used[i]
数组来表示第i
位是否已经被加入变进制序列,然后把变进制结果转换成十进制就可以了。
代码
这里我们以LuoguP5367 - 【模板】康托展开为例
1 | // luogu-judger-enable-o2 |
树状数组康托展开
我们发现,之前的康托展开是两重循环,时间复杂度$O(n^2)$
当$n \geq 10^4$时,暴力康托展开就承受不起了。
那么我们把used[]
数组的求解过程用树状数组来维护前缀和,然后就可以用log
的时间求出左侧小于自己的数的个数了。(没用线段树的原因是太难写了)
代码
1 | // luogu-judger-enable-o2 |
逆康托展开
我们先把rank--
作为第一轮的被除数。
然后从第1
位到第n
位依次循环,去除以(n-1)!
,用商找答案,用余数作为下一轮的被除数。
举个例子,求5
项数列中排名第5
的全排列:
$5-1=4$
- 4 / (5 - 1)! = 0 …… 4,所以第一个数是当前未出现的第0个数:1
- 4 / (4 - 1)! = 0 …… 4,所以第二个数是当前未出现的第0个数:2
- 4 / (3 - 1)! = 2 …… 0,所以第三个数是当前未出现的第2个数:5
- 0 / (2 - 1)! = 0 …… 0,所以第四个数是当前未出现的第0个数:4
- 0 / (1 - 1)! = 0 …… 0,所以第五个数是当前未出现的第0个数:3
所以答案就是:1,2,5,4,3
代码
1 | /* Headers */ |
康托展开的应用
康托展开其实可以当做一种变形的hash,它可以把一整个数列映射成一个数,且不会出错。
所以我们在遇到一些题目需要映射数列的时候,就可以利用康托展开来压缩空间。