硬币思考多重背包⭐

题面

给定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 << '\n';
	}
	return 0;
}
posted @ 2020-05-08 01:06  洛绫璃  阅读(186)  评论(0编辑  收藏  举报