问题描述:
有n种不同大小的数字ai,每种各mi个,判断是否可以从这些数字之中选出若干使它们的和恰好为K。(1 <=n <= 100 , 1 <= ai,mi <=100000 , 1 <= K <= 100000)
输入:
n = 3 ,K = 17
a = { 3 , 5 , 8}
m = { 3 , 2 , 2}
输出
Yes(3*3+8=17}
分析:
1.刻画一个最优解的结构特征:
定义dp[i+1][j]为前 i 个数是否能加和成 j2.递归地定义最优解的值:
为了让前 i 个数加和成j,那么前 i-1个数就需要加和成 j , j - ai , …. ,j - mi*ai中的某一种。
由此,递推关系如下:
dp[i+1][j]=(0 <= k <=mi且k*ai <= j时存在使dp[i][j-k*a]为真的k)3.计算最优解的值,采用自底向上的递推法。
代码如下:
#include<cstdio>
using namespace std;
const int maxn = 100;
int n,K;
int a[maxn],m[maxn];
bool dp[maxn][maxn];//dp[i+1][j]:用前i种数字是否能加和为j
void solve()
{
dp[0][0] = true;//没有数字加和当然为0了
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");
}
int main()
{
scanf("%d%d",&n,&K);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
for(int i=0;i<n;i++)
scanf("%d",&m[i]);
solve();
return 0;
}
这个算法时间复杂度为O(K∑imi),所以还需要优化
优化:
1.刻画一个最优解的结构特征:
重新定义dp[i+1][j]为前i种数加和得到 j 时第 i 种数最多还能剩余多少个(不能加和得到 i 的情况为 -1)2.递归地定义最优解的值:
如果前 i-1个数加和得到 j 的话,那么第 i 个数就不用加了,就剩下mi个。
如果前 i 种数加和出 j - ai时第 i 种数还剩下k的话,用这 i 种数加和 j 时第 i 种数就能剩下k - 1个。
dp[i+1][j]=- 1.mi (dp[i][j]>=0) ;
- 2.-1 (j < ai 或者dp[i+1][j-ai]<=0) ;
- 3.dp[i+1][j-ai]-1 (其他);
3.计算最优解的值,采用自底向上的递推法。
代码如下:
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 100000+10;
int dp[maxn],a[maxn],m[maxn];//dp[i+1][j]为用前i种数加和得到j时第i种数最多能剩余多少个
int main()
{
int n,K;
scanf("%d%d",&n,&K);
for(int i=0; i<n; i++)
{
scanf("%d",&a[i]);
}
for(int i=0; i<n; i++)
{
scanf("%d",&m[i]);
}
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];//如果前i-1个数加和能得到j的话,第i个数就可以留下mi个
}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");
return 0;
}