题解 P5367 【【模板】康托展开】

P5367 【模板】康托展开

感觉这题难度大概在绿题到蓝题之间qwq

一、洛谷日报[yummy]浅谈康托展开

如我想知道321是{1,2,3}中第几个小的数可以这样考虑 :

第一位是3,当第一位的数小于3时,那排列数小于321 如 123、 213 ,小于3的数有1、2 。所以有2× 2!个。再看小于第二位2的:小于2的数只有一个就是1 ,所以有1× 1!=1 所以小于321的{1,2,3}排列数有2×2!+1×1!=5个。所以321是第6个小的数。 2 ×2!+1× 1!+0× 0!就是康托展开。

再举个例子:1324是{1,2,3,4}排列数中第几个大的数:第一位是1小于1的数没有,是0个 0× 3! 第二位是3小于3的数有1和2,但1已经在第一位了,所以只有一个数2,1×2! 。第三位是2小于2的数是1,但1在第一位,所以有0个数 0×1! ,所以比1324小的排列有0×3!+1× 2!+0×1 !=2个,1324是第三个小数。(摘自百度)

暴力做法

//n表示全排列长度
for(int i=1;i<=n;i++)
{
    cin>>a[i];
    int x=a[i];
    for(int j=1;j<=a[i];j++)
        x-=used[j];
    //used[j]表示j是否用过(1用过,0没用)
    used[a[i]]=1;
    a[i]=x-1;
}

二、优化

“我们看到,刚才的方法有两重循环,时间复杂度为 O(N^2) ,找左侧用过的数的数量很费时间。

只要把used函数用线段树或树状数组维护区间和,就可以只花log的时间就求出左侧小于自己的数的个数了。” (摘自日报)

三、代码

#include<cstdio>
using namespace std;
#define N 1000001
int n,tr[N];
long long ans,fac[N];
inline void add(int x,int k) {
	for (; x<=n; x+=x&-x) tr[x]+=k;
}
inline int query(int x) {
	int t=0;
	for (; x; x-=x&-x) t+=tr[x];
	return t;
}
int main() {
	scanf("%d",&n);
	fac[0]=1;
	for (int i=1; i<=n; i++) {
		fac[i]=fac[i-1]*i%998244353;
		add(i,1);
	}
	for (int i=1,x; i<=n; i++) {
		scanf("%d",&x);
		ans=(ans+(query(x)-1)*fac[n-i])%998244353;
		add(x,-1);
	}
	printf("%lld",ans+1);
	return 0;
}

四、拓展:逆康托展开

例 {1,2,3,4,5}的全排列,并且已经从小到大排序完毕

(1)找出第96个数

首先用96-1得到95

用95去除4! 得到3余23

有3个数比它小的数是4

所以第一位是4

用23去除3! 得到3余5

有3个数比它小的数是4但4已经在之前出现过了所以第二位是5(4在之前出现过,所以实际比5小的数是3个)

用5去除2!得到2余1

有2个数比它小的数是3,第三位是3

用1去除1!得到1余0

有1个数比它小的数是2,第二位是2

最后一个数只能是1

所以这个数是45321(摘自百度)

原文地址:https://www.cnblogs.com/Randolph68706/p/11200202.html