注:文章内容源自《挑战程序设计竞赛》(第二版)
原题
多重部分和问题
有n种不同大小的数字ai,每种各mi个。判断是否可以从这些数字之中选出若干使它们的和恰好为K。
1<=n<=100
1<=ai,mi<=100000
1<=K<=100000
样例输入
n=3
a={3,5,8}
m={3,2,2}
K=17
样例输出
Yes(3*3+8=17)
涉及知识及算法
定义dp[i+1][j]为前i种数字是否能加和成j
为了用前i种数字加和成j,也就需要能用前i-1种数字加和成j,j-ai,…,j-mi*ai中的某一种。由此我们可以定义如下递推关系:
dp[i+1][j]=(0<=k<=mi且k*ai<=j时存在使dp[i][j-k*ai]为真的k)
代码
//数列的长度
int n;
//目标的和数
int K;
//值
int a[MAX_N];
//个数
int m[MAX_N];
bool dp[MAX_N+1][MAX_K+1];
void solve()
{
dp[0][0]=true;
for(int i=0;i<n;i++)
{
for(int j=0;j<=K;j++)
{
for(int k=0;k<=m[i]&&k*a[i]<=j;k++)
{
dp[i+1][j]=dp[i][j-k*a[i]];
}
}
}
if(dp[n][K])
{
printf("Yes\n");
}
else
{
printf("No\n");
}
}
这个算法并不够好,复杂度为O(K∑imi)。一般用DP求取bool结果会有不少浪费,同样的复杂度往往能获得更多信息。在这个问题中,我们不光求出能否得到目标的和数,同时把得到时ai这个数还剩下多少个可以使用 计算出来,这样就可以减少复杂度。
可以定义dp[i+1][j]为用前i种数加和得到j时第i种数最多能剩余多少个(不能加和得到i的情况下为-1)
这样如果前i-1个数加和能得到j的话,第i个数就可以留下mi个。此外,前i种数加和出j-ai时第i种数还剩下k(k>0)的话,用这i种数加和j时第i种数就能剩下k-1个。由此可得:
dp[i+1][j]={ mi
(dp[i][j]>=0)
{ -1
(j<ai或者dp[i+1][j-ai]<=0)
{ dp[i+1][j-ai]-1 (其他)
这样只要看最终是否满足dp[n][K]>=0就可以知道答案了。
这个递推式可以在O(nK)时间内计算出结果。再将数组重复利用,则可以得到:
int dp[MAX_K+1];
void solve()
{
memset(dp,-1,sizeof(dp));
dp[0]=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<=K;j++)
{
if(dp[j]>=0)
{
dp[j]=m[i];
}
else if(j<a[i]||dp[j-a[i]]<=0)
{
dp[j]=-1;
}
else
{
dp[j]=dp[j-a[i]]-1;
}
}
}
if(dp[K]>=0)
{
printf("Yes\n");
}
else
{
printf("No\n");
}
}