题面
给定N种硬币,其中第 i 种硬币的面值为Ai,共有Ci个。
从中选出若干个硬币,把面值相加,若结果为S,则称“面值S能被拼成”。
求1~M之间能被拼成的面值有多少个。
输入格式
输入包含多组测试用例。
每组测试用例第一行包含两个整数N和M。
第二行包含2N个整数,分别表示A1,A2,…,AN和C1,C2,…,CN。
当输入用例N=0,M=0时,表示输入终止,且该用例无需处理。
输出格式
每组用例输出一个结果,每个结果占一行。
数据范围
1≤N≤100,
1≤M≤105,
1≤Ai≤105,
1≤Ci≤1000
输入用例:
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
输出用例:
8
4
思考
先不看题
01背包
对于多重背包, 可以拆成01背包来做
unsigned int f[maxn];
memset(f, 0, sizeof f);
f[0] = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= c[i]; ++j)
for (int k = m; k >= v[i]; --k)
f[k] = max(f[k], f[k - v[i]] + w[i]);
int ans = 0;
for (int i = 0; i <= m; ++i)
ans = max(maxn, f[i]);
二进制拆法
(C_i)拆成p + 2个物品
(2^0) * (V_i), (2^1) * (V_i), ... , (2^p) * (V_i), (R_i) * (V_i)
虽然没队列优化快,但是好写
unsigned int f[maxn];
int nw[maxn * log2(maxn)], nv[maxn * log2(maxn)];
int cnt = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= c[i]; j <<= 1)
{
nw[++cnt] = j * w[i];
nv[cnt] = j * v[i];
c[i] -= j;
}
if (s[i]) nw[++cnt] = s[i] * w[i], nv[cnt] = s[i] * v[i];
}
memset(f, 0, sizeof f);
for (int i = 1; i <= cnt; ++i)
for (int j = m; j >= nv[i]; --j)
f[j] = max(f[j], f[j - nv[i]] + nw[i]);
单调队列
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2005;
int n, v[maxn], w[maxn], c[maxn], q[maxn], m, f[maxn];
int calc(int u, int i, int k)
{
return f[u + k * v[i]] - k * w[i];
}
int main()
{
scanf("%d%d", &n, &m);
memset(f, 0xcf, sizeof f);
f[0] = 0;
for (int i = 1; i <= n; ++ i)
{
scanf("%d%d%d", v + i, w + i, c + i);
for (int u = 0; u < v[i]; ++u)
{
int l = 1, r = 0;
int maxp = (m - u) / v[i];
for (int k = maxp - 1; k >= max(maxp - c[i], 0); -- k)
{
while (l <= r && calc(u, i, q[r]) <= calc(u, i, k)) --r;
q[++r] = k;
}
for (int p = maxp; p; --p)
{
while (l <= r && q[l] > p - 1) ++ l;
if (l <= r) f[u + p * v[i]] = max(f[u + p * v[i]], calc(u, i, q[l]) + p * w[i]);
if (p - c[i] - 1 >= 0)
{
while (l <= r && calc(u, i, q[r]) <= calc(u, i, p - c[i] - 1)) --r;
q[++r] = p - c[i] - 1;
}
}
}
}
int ans = 0;
for (int i = 1; i <= m; ++ i) ans = max(ans, f[i]);
printf("%d", ans);
return 0;
}
题解
好了来看题, 一定要注意到我们只关注可行性,而不关注最优解
暴力01背包
bool f[maxn];
memset(f, 0, sizeof);
f[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= c[i]; ++j)
for (int k = m; k >= a[i]; --k)
f[k] |= f[k - a[i]];
必定超时, 但是这道题是可行性为题, 不需要最优解
我们发现,若前i种硬币能凑出 sum/2, 只有两种情况:
1.在i阶段之前, 就已经f[j] = true
2.在i阶段之前, 就已经f[j - i] = true
于是,可以贪心, 设used[j] 表示f[j]在阶段i是为true的情况下至少需要多少块i种硬币
这样上面的代码 for(j) for(k) 循环可以优化为1维,直接正序扫面
当(!f[j] && f[j - i] && used[j - i] < a[i])才可以转移
巧妙利用其只求可行性,既不二分也不单调队列
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int a[101], c[101], n, m;
int f[maxn], cnt, used[maxn];
int main()
{
while (cin >> n >> m, n && m)
{
for (int i = 1; i <= n; ++i) cin >> a[i];
memset(f, 0, sizeof f);
f[0] = 1; cnt = 0;
for (int i = 1; i <= n; ++i)
{
cin >> c[i];
for (int j = 0; j <= m; ++j) used[j] = 0;
for (int j = a[i]; j <=m ; ++ j)
if (!f[j] && f[j - a[i]] && used[j - a[i]] < c[i])
f[j] = 1, used[j] = used[j - a[i]] + 1, ++cnt;
}
cout << cnt << '
';
}
return 0;
}