【算法进阶-康托展开】-C++

引入


这位老爷子就是康托

基本概念

康托展开是一个全排列到一个自然数的双射,常用于构建hash表时的空间压缩。设有n个数(1,2,3,4,…,n),可以有组成不同(n!种)的排列组合,康托展开表示的就是是当前排列组合在n个不同元素的全排列中的名次。

所以,康托展开是为了把一种全排列压缩成一个整数,它的实质是计算当前排列在所有由小到大全排列中的名次,因此是可逆的,称为逆康托展开(这篇博客不会讲)。

基本原理

我们用a[i]表示位于位置i后面的数小于A[i]值的个数。
公式长这样:

\(X=\sum_{i=1}^n a[i]×(n-i)!+1\)
也就是:
\(X = A[0] × (n-1)! + A[1] × (n-2)! + … + A[n-1] × 0!\)
这个算出来的数康拖展开值,是在所有排列次序 - 1的值,因此X+1即为在全排列中的次序,最终输出的应该是X+1
举个栗子:
在(1,2,3,4,5)5个数的排列组合中,计算 34152的康托展开值。
带入上面的公式
X=2*4!+2*3!+0*2!+1*1!+0*0! =>X=61
最后的结果也就是62.

康托展开基础-核心代码

//返回数组a中当下顺序的康托
int cantor(int *a,int n)
{
	int ans=0;
	for(int i=0;i<n;i++)
	{
		int x=0,c=1,m=1;
		for(int j=i+1;j<n;j++)
		{
			if(a[j]<a[i])x++;
			m*=c;
			c++;
		}
		ans+=x*m;
	}
	return ans;
}

注意:这个函数返回的是X,输出请输出X+1,理由上面说过了.

康托展开的优化

很明显,上面讲的方法很暴力 需要\(O(n^2)\)的时间才能跑出来。
怎么优化呢?
首先我们可以了解,对于第\(i\)个位置,若该位置的数是未出现在之前位置上的数中第\(k\)大的,那么有\((k-1) \times (N-i)!\)种方案是该位置上比这个排列小的,所以总排名比该排列小。
由此我们可以得到,该排列的排名等于\(\sum_{i=1}^{N}(a[a_i]-1) \times (N-i)!\)
p.s.内层\(a_i\)表示给出的排列中的第i个数,重名不要管因为上文就是用的a数组计排名
阶乘问题因为每次都来计算太费事,所以预处理一下就可以了。
根据思路就可以用树状数组水过去模板了
不会树状数组的向这里看

代码:

#include<bits/stdc++.h>
#define FAST_IN std::ios::sync_with_stdio(false);cin.tie(NULL);
#define MOD 998244353
using namespace std;
long long fac[1000010],a,n,tree[1000001],ans;
int lowbit(int k)
{
	return k&-k;
}
long long ask(long long s)
{
    long long ans=0;
    for(long long i=s;i>=1;i-=lowbit(i))
		ans+=tree[i];
    return ans;
}
void add(int s,int num)
{
	for(long long i=s;i<=n;i+=lowbit(i))
		tree[i]+=num;
}
void cal()
{
	fac[0]=1;
	for(int i=1;i<=n;i++)
	{ 
		fac[i]=fac[i-1]*i%MOD;
		add(i,1);
	}
}
int main()
{
	FAST_IN;
	cin>>n;
	cal();
	for(int i=1;i<=n;i++)
	{ 
		cin>>a;
		ans=(ans+(ask(a)-1)*fac[n-i]%MOD)%MOD;
		add(a,-1);
	}
	cout<<ans+1<<endl;
	return 0;
}

模板题传送门

ov.

posted @ 2019-07-23 17:16  摸鱼酱  阅读(967)  评论(0编辑  收藏  举报