洛谷 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 }

 

posted @ 2019-08-12 21:54  尹昱钦  阅读(338)  评论(1编辑  收藏  举报