1. 程式人生 > >[POJ1742] Coins [多重揹包]

[POJ1742] Coins [多重揹包]

[ L i n k \frak{Link} ]


三種方法。
寫得比較優秀的二進位制拆分+分資料範圍處理、多重揹包可行性、單調佇列優化。


第一種方法。
一般的二進位制拆分複雜度是 Θ ( m l o g 2

C i ) \mathcal{\Theta(m\sum log_2C_i)} 。算一下就能發現這個上界卡到 1 e
8 \mathcal{1e8}

考慮怎麼降低處理數量多的硬幣的複雜度。顯然假如 C i × V i m \mathcal{C_i×V_i\ge m} 就可以當作完全揹包來跑。
那就這麼做。

注意多重揹包的二進位制拆分是允許重複的,
如果是單純把 C i \mathcal{C_i} 二進位制表示裡為 1 \mathcal{1} 的位拿出來並不能組合得到所有不大於 C i \mathcal{C_i} 的方案
一定要從 1 \mathcal{1} 開始倍增,最後把減剩下的 C i \mathcal{C_i} 單獨拿出來再跑一次。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#include<cstring>
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<cstdio>
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#include<cstring>
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 &ThickSpace; &ThickSpace; k [ 0 , c o u n t i ] } \mathcal{f_j=max\{f_{j-kcost_i}+kvalue_i\;|\;k\in[0,count_i]\}}
外層列舉物品,把 j \mathcal{j} 重標號,按照 % c o s t i \mathcal{\%cost_i} 的餘數分類,轉化式子。可以得到
f j = m a x { f k k v a l u e &ThickSpace; &ThickSpace; k [ j c o u n t i , j ] } + j v a l u e , &ThickSpace; j c o s t i + R e m a i n d e r = j \mathcal{f_{j&#x27;}=max\{f_{k&#x27;}-k&#x27;value\;|\;k&#x27;\in[j&#x27;-count_i,j&#x27;]\}+j&#x27;value,\;j&#x27;cost_i+Remainder=j} (列舉 R e m a i n d e r \mathcal{Remainder}
這是單調佇列優化的普遍形式,在單調佇列中維護 m a x { f k k v a l u e } , &ThickSpace; k [ j c o u n t i , j ] \mathcal{max\{f_{k&#x27;}-k&#x27;value\},\;k\in[j-count_i,j]}
每次將 f k k v a l u e \mathcal{f_{k&#x27;}-k&#x27;value} 入隊,得到的最大值是 q u e u e . f r o n t ( ) + j v a l u e \mathcal{*queue.front()+jvalue}
到這道題目裡面有 c