(不易)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;
}