【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;
}
posted @ 2018-03-31 09:38  ywwyww  阅读(397)  评论(1编辑  收藏  举报