题目大意:传说中的男人八题,是男人就A这八题。有n种面额的硬币,面额个数分别为A_i、C_i,求最多能搭配出几种不超过m(1-m)的金额?
题目链接:点击打开链接
分析:
①首先来看看朴素的方法:
bool dp[i][j] := 用前i种硬币能否凑成j
递推关系式:
dp[i][j] = (存在k使得dp[i – 1][j – k * A[i]]为真,0 <= k <= C_i且下标合法)
然后三重循环ijk递推
这样的话复杂度为O(m*ΣC_i),肯定过不去。。
②然后我们想到将问题转化为01背包,并利用二进制(具体可以看背包九讲)来优化
复杂度为O(m*ΣlogC_i),仍然TLE。。
③我在钻研完全背包问题与多重背包问题(不仅是背包)时,曾不断想要将第①种方法中的k那一层循环利用与完全背包一样的方法除去,最终通过不断努力,终于找到了完全问题与多重问题之间在优化递推式时质的区别!!!发现通过自己这种方法也可以在O(nm)的复杂度里得出最终结果,只是苦于这道题的时间实在是卡得太紧,用标准方法(下面要讲的方法④)也花了2000ms,由于自己这种方法可能多了1一个常数倍的时间,所以最终还是TLE了。虽然很可惜没有自己A掉,但却对完全与多重背包有了一个更加深层次的理解,等刷完手上的几个背包问题,我将会写一篇文章来专门为大家阐述这种方法。
④对于朴素的方法,这个算法每次只记录一个bool值,损失了不少信息。在这个问题中,不光能够求出是否能得到某个金额,同时还能把得出了此金额时A_i还剩下多少个算出来,这样直接省掉了k那重循环。
我们优化dp的状态:
状态:dp[i][j] : = 用前i种硬币凑成j时第i种硬币最多能剩余多少个( - 1表示配不出来)
转移:
①若dp[i-1][j]>=0,即前i-1种可以配成j,所以根本用不到第i种,所以剩余C_i种 dp[i][j]=C_i
②若j<a[i] || dp[i][j-a[i]]<=0,由于dp[i-1][j]<0,所以要想配成j起码得要有第i种,若j<a[i]则第i种用不到,所以前i种仍配不到j,若dp[i][j-a[i]]<=0,则说明配成j-a[i]时第i种已经无剩余或者甚至无法配成j-a[i],更别说配成j了, dp[i][j]=-1
③其他情况,由于a[i]还有剩,所以dp[i][j]相当于在dp[i][j-a[i]]的基础上多使用了一个a[i],此时 dp[i][j]=dp[i][j-a[i]]-1
最终找出所有>=0的dp[n][i]个数就行了(1<=i<=m)
附上朴素版,我自己的版本与最终AC版:
#include <iostream> //朴素版
#include <algorithm>
using namespace std;
bool dp[100 + 16][100000 + 16]; // dp[i][j] := 用前i种硬币能否凑成j
int A[100 + 16];
int C[100 + 16];
int main(int argc, char *argv[])
{
int n, m;
while (cin >> n >> m && n > 0)
{
memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; ++i)
cin >> A[i];
for (int i = 0; i < n; ++i)
cin >> C[i];
dp[0][0] = true;
for (int i = 0; i < n; ++i)
for (int j = 0; j <= m; ++j)
for (int k = 0; k <= C[i] && k * A[i] <= j; ++k)
dp[i + 1][j] |= dp[i][j - k * A[i]];
int answer = count(dp[n] + 1, dp[n] + 1 + m, true); // 总额0不算在答案内
cout << answer << endl;
}
return 0;
}
#include<iostream> //我自己的版本
using namespace std;
int a[105], c[105];
int n, m;
long long dp[2][100005]; //dp[i][j]表示前i个可以以多少种方式组成j
int main()
{
while (scanf("%d%d", &n, &m) && n && m)
{
int ans = 0;
memset(dp, 0, sizeof dp);
dp[0][0] = 1;
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
scanf("%d", &c[i]);
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
dp[i % 2][j] = dp[(i - 1) % 2][j] + (j >= a[i] ? dp[i % 2][j - a[i]] : 0) - (j >= (c[i] + 1) * a[i] ? dp[(i - 1) % 2][j - (c[i] + 1)*a[i]] : 0);
for (int i = 1; i <= m; i++)
ans += dp[n % 2][i] > 0;
printf("%d\n", ans);
}
return 0;
}
#include<iostream> //最终AC版
#include<algorithm>
using namespace std;
int a[105], c[105];
int n, m;
int dp[100005]; //dp[i][j]表示用前i种硬币凑成j时第i种硬币最多能剩余多少个( - 1表示配不出来)
int main()
{
while (scanf("%d%d", &n, &m) && n && m)
{
int ans = 0;
memset(dp, -1, sizeof dp);
dp[0] = 0;
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
scanf("%d", &c[i]);
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
if (dp[j] >= 0) dp[j] = c[i];
else if (j < a[i] || dp[j - a[i]] < 0) dp[j] = -1;
else dp[j] = dp[j - a[i]] - 1;
if (i == n&&j > 0) ans += dp[j] >= 0;
}
printf("%d\n", ans);
}
return 0;
}