P4463 [集训队互测 2012] calc
前置知识
拉插,dp。
思路
-
考虑对于一个序列我们可以将它从小到大排序,然后这些序列的个数就是序列长度的阶乘。为什么要排序,因为考虑 dp 状态里要有用了哪些数,如果不按任何顺序,那么数两两不同的限制就很难刻画,有了排序之后就只需要填更大的数。
-
设 \(dp_{i,j}\) 表示前 \(i\) 个数用了 \(1-j\) (值域范围)的序列的值的和。然后考虑转移,\(dp_{i,j}=dp_{i-1,j-1}*j+dp_{i,j-1}\) 表示这一位填 \(j\) 或者只用 \(j-1\) 以前的方案(继承)。
-
这样 \(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\) 。
-
推出是几次多项式有什么用?如果你还记得知道 \(n+1\) 个点就可以唯一确定一个 \(n\) 次多项式,那么对于 \(dp_{n,i}\) 我们也可以通过 dp 算出 \(2n+1\) 个点来求出这个多项式,然后直接代入 \(k\) ,就可以得到在 \(k\) 处的取值。
-
最后,对于给出点求出多项式,不就是我们拉插解决的问题,因为 \(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';
}

浙公网安备 33010602011771号