【2019雅礼集训】【CF 960G】【第一类斯特林数】【NTT&多项式】permutation


目录
  • 题意
  • 输入格式
  • 输出格式
  • 思路
  • 代码

题意

找有多少个长度为n的排列,使得从左往右数,有a个元素比之前的所有数字都大,从右往左数,有b个元素比之后的所有数字都大。
n<=2*10^5,a,b<=n

输入格式

输入三个整数n,a,b。

输出格式

输出一个整数,表示答案。

思路

这道题是真的神啊...

首先,根据官方题解的思路,首先有一个n^2的DP:
定义dp[i][j]表示一个长度为i的排列,从前往后数一共有j个数字大于所有排在它前面的数字。
首先有转移式:

\[dp[i][j]=dp[i-1][j-1]+(i-1)*dp[i-1][j] \]

怎么理解这个式子呢?
首先,最后的排列一定是这样一个形式:

中间那个是最大值(n),那么前面第一个位置到第二个位置之间不能放任意一个数;第二个位置与第三个位置之间能够放1~ 4之间的数;第三个与第四个之间能够放4~9...我们能够发现,相邻两个位置能够放的数一定是小于前一个位置的。那么我们就可以根据选中的关键点(如上图中的1,4,9)将前半部分的序列分为几部分,每一部分的代表元素为这一部分中的数字的最大值。

例如:1-423-95678,就可以看成是3个部分。代表元素分别是1,4,9。

根据定义,现在考虑的是一共有i个数字,分成了j段。考虑加入一个新的最小的数字,考虑它放在哪里:

  1. 放在开头,自己成为一个新的部分,就由dp[i-1][j-1]转移而来。
    2.因为是最小的,所以可以放在之前的所有已经存在的部分中,那么有(i-1)中方案,就由(i-1)*dp[i-1][j]转移而来。

这样子就有了dp的转移式,很显然最后的答案就是:

\[Ans=\sum_{i=1}^{n}(dp[i][a-1]*dp[n-i-1][b-1])*C_{n-1}^{i-1} \]

其实就是枚举最大值的位值i,然后从剩下的n-1中选出i-1个,再将这i-1个数字分为a-1个部分,后面的n-i-1个位置分为b-1个部分。那就有\(O(n^2)\)的算法了。


有了上述的式子之后,接下来就比较好处理了。

仔细观察dp的转移式,我们会惊奇的发现它竟然和第一类斯特林数的递推式是一样的。也就是:

\[dp[i][j]=[^{i}_{j}] \]

第一类斯特林数是将i个数分成j个圆排列的方案数(忽略顺序的前提下)。而我们可以把之前定义的"部分"每一个都看成是一个圆排列,每一个都把其中最大的值通过圆排列转到这一部分开头的位置,就完美的对应上了。

而在全局看,我们可以先将n-1个数字分配当a+b-2个圆排列里面去,然后再将这a+b-2分成左边a-1个和右边b-1个,就是简单组合数了。那么答案式就可以进一步化简为:

\[Ans=[_{a+b-2}^{\ \ n-1}]*C_{a+b-2}^{a-1} \]

这样子我们的主要问题就变为求前面那个斯特林数就可以了。


而如何快速求斯特林数又是另外一个问题了...
感觉这里写一遍的话,好像有些冗长了。就在下面贴了一个链接,在里面我会尽可能详细的讲解如何快速求解S(n,k)。

代码

#include
#include
#include
#define MAXN 600000
#define MO 998244353
#define G 3
using namespace std;
int seq[MAXN+5];
int n,a,b;
int fact[MAXN+5],inv[MAXN+5];
int PowMod(int x,int y)
{
	int ret=1;
	while(y)
	{
		if(y&1)
			ret=1LL*ret*x%MO;
		x=1LL*x*x%MO;
		y>>=1;
	}
	return ret;
}
void Prepare()
{
	fact[0]=1;
	for(int i=1;i<=MAXN;i++)
		fact[i]=1LL*fact[i-1]*i%MO;
	inv[MAXN]=PowMod(fact[MAXN],MO-2);
	for(int i=MAXN-1;i>=0;i--)
		inv[i]=1LL*inv[i+1]*(1LL*i+1LL)%MO;
}
void Reverse(int A[],int deg)
{
	for(int i=0;i>=1,~j&s;);
		if(i=1;i--)
			B[i]=(1LL*B[i]*(1LL*deg-1LL)%MO+1LL*B[i-1])%MO;
}
int C(int x,int y)
{
	return 1LL*fact[x]*inv[y]%MO*inv[x-y]%MO;
}
int main()
{
	Prepare();
	scanf("%d %d %d",&n,&a,&b);
	if(n==1&&a==1&&b==1)
//注意加特判,也可以通过改变Solve底层的返回条件来兼容这种情况
		printf("1\n");
	else if((n>1&&a==1&&b==1)||a==0||b==0)
		printf("0\n");
	else
	{
		Solve(n-1,seq);//快速求第一类斯特林数(nlogn)
		int part1=seq[a+b-2];
		int ans=1LL*C(a+b-2,a-1)*part1%MO;
		printf("%d\n",ans);
	}
	return 0;
}
/*
2 2 1

5 2 2

*/