[POJ1742] Coins [多重背包]

[ L i n k \frak{Link} Link]


三种方法。
写得比较优秀的二进制拆分+分数据范围处理、多重背包可行性、单调队列优化。


第一种方法。
一般的二进制拆分复杂度是 Θ ( m ∑ l o g 2 C i ) \mathcal{\Theta(m\sum log_2C_i)} Θ(mlog2Ci)。算一下就能发现这个上界卡到 1 e 8 \mathcal{1e8} 1e8
考虑怎么降低处理数量多的硬币的复杂度。显然假如 C i × V i ≥ m \mathcal{C_i×V_i\ge m} Ci×Vim就可以当作完全背包来跑。
那就这么做。

注意多重背包的二进制拆分是允许重复的,
如果是单纯把 C i \mathcal{C_i} Ci二进制表示里为 1 \mathcal{1} 1的位拿出来并不能组合得到所有不大于 C i \mathcal{C_i} Ci的方案
一定要从 1 \mathcal{1} 1开始倍增,最后把减剩下的 C i \mathcal{C_i} Ci单独拿出来再跑一次。

#include
#include
#include
#include
#include
#include
using namespace std;
int n, m, c;
int a[105];
bool f[100005];
int main() {
	while (~scanf("%d%d", &n, &m)) {
		if (!n && !m) return 0;
		for (int i = 1; i <= m; ++i) {
			f[i] = 0;
		}
		f[0] = 1;
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &a[i]);
		}
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &c);
			if (c * a[i] < m) {
				for (int t, k = 1; k <= c; k <<= 1) {
					t = k * a[i];
					for (int j = m; j >= t; --j) {
						f[j] |= f[j - t];
					}
					c -= k;
					if (!c) break;
				}
				if (c) {
					int t = c * a[i];
					for (int j = m; j >= t; --j) {
						f[j] |= f[j - t];
					}
				}
			}
			else {
				for (int j = a[i]; j <= m; ++j) {
					f[j] |= f[j - a[i]];
				}
			}
		}
		int ans = 0;
		for (int i = 1; i <= m; ++i) ans += f[i];
		printf("%d\n", ans);
	}
	return 0;
}

第二种方法。
实际上这种方法是很自然的。
我的代码比较丑,漂亮一点的代码应该看一眼就可以明白原理了。

#include
#include
#include
#include
#include
#include
using namespace std;
int n, m, c, ans;
int a[105];
int vis[100005];
bool f[100005];
void Check(const int &x) {
	for (int i = 1; i <= m; ++i) {
		vis[i] = 0;
	}
	f[0] = 1;
	for (int i = a[x]; i <= m; ++i) {
		if (vis[i - a[x]] == c) continue;
		if (f[i] || !f[i - a[x]]) continue;
		f[i] = 1;
		vis[i] = vis[i - a[x]] + 1;
		++ans;
	}
}
int main() {
	while (~scanf("%d%d", &n, &m)) {
		if (!n && !m) return 0;
		ans = 0;
		memset(f, 0, sizeof(f));
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &a[i]);
		}
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &c);
			Check(i);
		}
		printf("%d\n", ans);
	}
	return 0;
}

第三种方法。
实际上这道题目完全没必要用单队解、不过也是可以的。
多重背包朴素式子 f j = m a x { f j − k c o s t i + k v a l u e i    ∣    k ∈ [ 0 , c o u n t i ] } \mathcal{f_j=max\{f_{j-kcost_i}+kvalue_i\;|\;k\in[0,count_i]\}} fj=max{fjkcosti+kvalueik[0,counti]}
外层枚举物品,把 j \mathcal{j} j重标号,按照 % c o s t i \mathcal{\%cost_i} %costi的余数分类,转化式子。可以得到
f j ′ = m a x { f k ′ − k ′ v a l u e    ∣    k ′ ∈ [ j ′ − c o u n t i , j ′ ] } + j ′ v a l u e ,    j ′ c o s t i + R e m a i n d e r = j \mathcal{f_{j'}=max\{f_{k'}-k'value\;|\;k'\in[j'-count_i,j']\}+j'value,\;j'cost_i+Remainder=j} fj=max{fkkvaluek[jcounti,j]}+jvalue,jcosti+Remainder=j(枚举 R e m a i n d e r \mathcal{Remainder} Remainder
这是单调队列优化的普遍形式,在单调队列中维护 m a x { f k ′ − k ′ v a l u e } ,    k ∈ [ j − c o u n t i , j ] \mathcal{max\{f_{k'}-k'value\},\;k\in[j-count_i,j]} max{fkkvalue},k[jcounti,j]
每次将 f k ′ − k ′ v a l u e \mathcal{f_{k'}-k'value} fkkvalue入队,得到的最大值是 ∗ q u e u e . f r o n t ( ) + j v a l u e \mathcal{*queue.front()+jvalue} queue.front()+jvalue
到这道题目里面有 c o s t i = v a l u e i \mathcal{cost_i=value_i} costi=valuei,于是每个背包只有能否被覆盖的状态
于是单调队列维护的就是有没有 1 \mathcal{1} 1
那么只要知道前面这一段有没有 1 \mathcal{1} 1就可以了。

你可能感兴趣的:(背包dp,dp)