1. 程式人生 > >(不易)POJ-1742 多重部分和,多重揹包可行性

(不易)POJ-1742 多重部分和,多重揹包可行性

題目大意:傳說中的男人八題,是男人就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;
}