【XSY2759】coin DP 线性插值
题目描述
有\(n\)种面值不同的硬币,每种有无限个,且任意两个\((x,y)\)要么\(x\)是\(y\)的倍数,要么\(y\)是\(x\)的倍数。
你要取\(m\)元钱,问你有多少种不同的取法。
\(n\leq 50,m\leq {10}^{18}\)
题解
假设面值为\(a_1,a_2,\ldots,a_n\)
先把所有硬币按面值从小到大排序。
那么考虑从小到大取钱。
如果前面\(i\)种面值已经取完了,那么取的钱数\(\bmod\)\(a_{i+1}\)已经确定了。
有这么一个DP:设\(f_i(x)\)为取完了前面\(i\)种面值的硬币,取的钱数为\(xa_i+m\bmod a_i\)的方案数。
转移:枚举\(i\)这种硬币用了多少个(或者说剩下了多少个):
\[\begin{align}
f_i(x)&=\sum_{j=0}^x f_{i-1}(\frac{ja_i+m\bmod a_i}{a_{i-1}})\\
&=\sum_{j=0}^x f_{i-1}(bj+c)\\
\end{align}
\]
我们很容易发现\(f_i(x)\)是一个\(i\)次函数。
那么只需要求\(f_i(0)\ldots f_i(i)\)就可以了。
每次可以通过线性插值求出。
时间复杂度:\(O(n^3)\)
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int p=998244353;
ll fp(ll a,ll b){ll s=1;for(;b;b>>=1,a=a*a%p)if(b&1)s=s*a%p;return s;}
int f[60][60];
int ifac[60];
int inv[60];
int pp[60];
int *pre=pp+1;
int suf[60];
ll a[60];
int n;
ll m;
int c[60];
int gao(int id,ll x)
{
if(x<=id)
return f[id][x];
x%=p;
pre[-1]=1;
for(int i=0;i<=id;i++)
pre[i]=ll(x-i)*pre[i-1]%p;
suf[id+1]=1;
for(int i=id;i>=0;i--)
suf[i]=ll(x-i)*suf[i+1]%p;
ll s=0;
for(int i=0;i<=id;i++)
s+=(ll)f[id][i]*pre[i-1]%p*suf[i+1]%p*ifac[i]%p*ifac[id-i]%p*((id-i)&1?-1:1);
return s%p;
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
#endif
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
ifac[0]=ifac[1]=inv[1]=1;
for(int i=2;i<=n;i++)
{
inv[i]=(ll)-p/i*inv[p%i]%p;
ifac[i]=(ll)ifac[i-1]*inv[i]%p;
}
f[1][0]=1;
f[1][1]=1;
for(int i=2;i<=n;i++)
for(int j=0;j<=i;j++)
f[i][j]=(f[i][j-1]+gao(i-1,(j*a[i]+m%a[i])/a[i-1]))%p;
int ans=gao(n,m/a[n]);
ans=(ans+p)%p;
printf("%lld\n",ans);
return 0;
}