P4463 [集训队互测 2012] calc

前置知识

拉插,dp。

思路

  1. 考虑对于一个序列我们可以将它从小到大排序,然后这些序列的个数就是序列长度的阶乘。为什么要排序,因为考虑 dp 状态里要有用了哪些数,如果不按任何顺序,那么数两两不同的限制就很难刻画,有了排序之后就只需要填更大的数。

  2. \(dp_{i,j}\) 表示前 \(i\) 个数用了 \(1-j\) (值域范围)的序列的值的和。然后考虑转移,\(dp_{i,j}=dp_{i-1,j-1}*j+dp_{i,j-1}\) 表示这一位填 \(j\) 或者只用 \(j-1\) 以前的方案(继承)。

  3. 这样 \(dp\) 就可以做了,但是时间复杂度为 \(O(nk)\) 。由于 \(k\) 非常的大,所以不能通过此题。我们要想一个办法去掉时间复杂度里的 \(k\) 。首先我们知道答案 \(dp_{n,j}\) 显然可以理解为一个多项式,我们设它是关于 \(i\)\(g(n)\) 次多项式。现在我们不知道这个 \(g(n)\) 的表达式 ,但是我们可以根据转移来推。

    \[dp_{n,i}-dp_{n,i-1}=dp_{n-1,i-1}\times i \]

    左边是 差分 的形式是关于 \(i\)\(g(n)-1\) 次多项式,右边是关于 \(i\)\(g(n-1)+1\) 。左右两边的次数是相同的,同时有边界是 \(g(0)=0\) ,所以我们可以发现 \(g\) 是一个等差数列,可以直接得出 \(g(n)=2n\)

  4. 推出是几次多项式有什么用?如果你还记得知道 \(n+1\) 个点就可以唯一确定一个 \(n\) 次多项式,那么对于 \(dp_{n,i}\) 我们也可以通过 dp 算出 \(2n+1\) 个点来求出这个多项式,然后直接代入 \(k\) ,就可以得到在 \(k\) 处的取值。

  5. 最后,对于给出点求出多项式,不就是我们拉插解决的问题,因为 \(n\le 500\) 所以暴力做就可以。(不会拉插的话,可以点这里

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e3+10;
int y[N],jc[N],f[N][N],k,n,m,mod; 
int ksm(int x,int y)
{
	int res=1;
	while(y)
	{
		if(y&1)res=res*x%mod;
		y>>=1;
		x=x*x%mod;
	}
	return res;
}
int get()
{
	int ans=0;
	for(int i=1;i<=m;i++)
	{
		int ans1=1,ans2=1;
		for(int j=1;j<=m;j++)
		{
			if(i==j)continue;
			ans1=ans1*((k-j+mod)%mod)%mod;
			ans2=ans2*((i-j+mod)%mod)%mod;
		}
		ans=(ans+y[i]*ans1%mod*ksm(ans2,mod-2)%mod)%mod;
	}
	return ans;
}
signed main()
{
	cin>>k>>n>>mod;
	jc[0]=1;
	for(int i=1;i<=n;i++)jc[i]=jc[i-1]*i%mod; 
	m=2*n+1;
	for(int i=0;i<=m;i++)f[0][i]=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			f[i][j]=(f[i-1][j-1]*j%mod+f[i][j-1])%mod;
			if(i==n)y[j]=f[i][j];
		}
	cout<<jc[n]*get()%mod<<'\n';
}
posted @ 2025-05-07 21:54  exCat  阅读(20)  评论(0)    收藏  举报