洛谷 P5367 【模板】康托展开(数论,树状数组)
题目链接
https://www.luogu.org/problem/P5367
什么是康托展开
百度百科上是这样说的:
“康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。”
是不是讲得很精(meng)致(bi)呢?
我看了无数篇博客,终于明白了一点点。
其实,康托展开就是求一个全排列在所有全排列中字典序排名第几。
举个例子:
比如说n=3的一个全排列:2 1 3 它的排名是3。
我们列出所有的全排列:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
显然,2 1 3在里面字典序排名第三。
暴力求法(基本思路)
首先我们用a[i]表示原数的第i位在当前未出现的元素中是排在第几个
比如说 "2 3 4 1"
a[1]=2 a[2]=2 a[3]=2 a[4]=0
拿a[2]举例子,到第二位时,未出现的数字有1,3,4,显然3排在第二位上,所以a[2]=2。
然后我们想,在前k-1位相等的情况下,a[k]具有什么意义?比当前情况字典序小的全排列数有多少呢?
显然是 a[k]*(k-1)! (注意阶乘的优先级比乘法运算高) 哪里显然了QAQ?
好像这叫做乘法原理来着(蒟蒻记不清楚了)
a[k]是第k位的比原排列小的数字数量,而第k-1~n位无论是什么数一定小于原数列,而且每一位都要用掉一个数字,所以就是a[k]*(k-1)*(k-2)*(k-3)*……*2*1。
最后把这些小于原排列的排列数量加起来,最后在+1就是原数列的排名。
放公式:ans=a1*0+a2*(2-1)!+a3*(3-1)!+……+an*(n-1)!+1。
时间复杂度为O(n^2)
优化
- 先预处理1到n的阶乘
- 用树状数组来维护有多少个未出现的比自己小的数(单点修改,区间查询)——一开始所有点都修改为1,然后每遇到一个点,就修改为0,最后查询1~s[k-1]有多少个1就行了(s为原数列)。
当然了,也可以用万能的线段树(只不过常数比较大罢了)
AC代码
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 const int mod=998244353; 5 const int maxn=1000005; 6 int ss[maxn],a[maxn],s[maxn],n; 7 inline int lowbit(int x){ 8 return x&(-x); 9 } 10 void update(int id,int x){ 11 for(int i=id;i<=n;i+=lowbit(i)){ 12 s[i]+=x; 13 } 14 } 15 int query(int id){ 16 int res=0; 17 for(int i=id;i>0;i-=lowbit(i)){ 18 res+=s[i]; 19 } 20 return res; 21 } 22 long long ans,jc[maxn]; 23 int main() 24 { 25 cin>>n; 26 jc[1]=1; 27 for(int i=2;i<n;i++) jc[i]=jc[i-1]*i%mod; 28 for(int i=1;i<=n;i++) scanf("%d",&ss[i]); 29 for(int i=1;i<=n;i++) update(ss[i],1); 30 for(int i=1;i<=n;i++){ 31 update(ss[i],-1); 32 a[n-i+1]=query(ss[i]); 33 } 34 for(int i=1;i<=n;i++) ans=(ans+(long long)a[i]*jc[i-1]%mod)%mod; 35 cout<<ans+1; 36 return 0; 37 }