POJ #1384 Piggy-Bank 深入理解完全背包及其优化
Description
Before ACM can do anything, a budget must be prepared and the necessary financial support obtained. The main income for this action comes from Irreversibly Bound Money (IBM). The idea behind is simple. Whenever some ACM member has any small money, he takes all the coins and throws them into a piggy-bank. You know that this process is irreversible, the coins cannot be removed without breaking the pig. After a sufficiently long time, there should be enough cash in the piggy-bank to pay everything that needs to be paid.
But there is a big problem with piggy-banks. It is not possible to determine how much money is inside. So we might break the pig into pieces only to find out that there is not enough money. Clearly, we want to avoid this unpleasant situation. The only possibility is to weigh the piggy-bank and try to guess how many coins are inside. Assume that we are able to determine the weight of the pig exactly and that we know the weights of all coins of a given currency. Then there is some minimum amount of money in the piggy-bank that we can guarantee. Your task is to find out this worst case and determine the minimum amount of cash inside the piggy-bank. We need your help. No more prematurely broken pigs!Input
The input consists of T test cases. The number of them (T) is given on the first line of the input file. Each test case begins with a line containing two integers E and F. They indicate the weight of an empty pig and of the pig filled with coins. Both weights are given in grams. No pig will weigh more than 10 kg, that means 1 <= E <= F <= 10000. On the second line of each test case, there is an integer number N (1 <= N <= 500) that gives the number of various coins used in the given currency. Following this are exactly N lines, each specifying one coin type. These lines contain two integers each, Pand W (1 <= P <= 50000, 1 <= W <=10000). P is the value of the coin in monetary units, W is it's weight in grams.Output
Print exactly one line of output for each test case. The line must contain the sentence "The minimum amount of money in the piggy-bank is X." where X is the minimum amount of money that can be achieved using coins with the given total weight. If the weight cannot be reached exactly, print a line "This is impossible.".Sample Input
3 10 110 2 1 1 30 50 10 110 2 1 1 50 30 1 6 2 10 3 20 4Sample Output
The minimum amount of money in the piggy-bank is 60. The minimum amount of money in the piggy-bank is 100. This is impossible.
思路
题目求的是前 N 种物品放入空间 V(V=F-E) 的背包的最小权值,每种物品可无限件使用,典型的完全背包题目。我们不妨先假设求的是最大价值,捋捋思路。
完全背包和01背包很相似,不同的是每一种物品都可以无限拿取,决策不再是“取 or 不取”两种,而是变成了取 0 件、取 1 件、取 2 件..等等,非常多种。
如果定义原问题为状态,即 dp[i][j] 表示前 i 种物品放入空间为 j 的背包的最大价值,有如下状态转移方程:
// dp[i-1][j-k*w[i]] + k*v[i] 表示放入k个第i种物品所得到的最大价值 dp[i][j] = max{ dp[i-1][j-k*w[i]] + k*v[i] | 0 <= k*w[i] <= j }
注意哦,这里的 i 和01背包递推式中的 i 意义并不一样,这里的 i 表示第 i 种物品,而01背包的 i 表示第 i 件物品。
和原来一样,总共有 N·V 个状态,但是求解每个状态的时间不再是常数,而是 O(V/w[i]) ,且直接这么写算法的话是要用三重循环,所以时间复杂度大于 O(N·V) 。
那么,如何优化呢?
再分析一下上面的递推式,发现和01背包一样,第 i 行的数据只与第 i-1 行的数据有关,在01背包里我们是这么处理的:利用两个一维数组滚动存储 i 阶段的状态与 i -1 阶段的状态。或者,再优化用一个一维数组滚动存储。在完全背包问题中其实也是可以的。
现在给出一维滚动数组实现的算法,时间复杂度是 O(N·V):
for i = 1..N for j = 0..V dp[j] = max (dp[j], dp[j-w[i]] + v[i] )
和01背包一维数组实现的算法的唯一区别就是内循环的次序。为什么完全背包的内循环是顺序的?
我们先来想想01背包里内循环为什么是逆序的,因为当前状态 dp[i][j] 是由前一状态 dp[i-1][j] 、dp[i-1][j-w[i]] 递推而来的,若内循环逆序就可以保证 max 中的 dp[j] 、dp[j-w[i]] 是前一状态的值。换句话说,就是为了保证每件物品只被选一次,保证在做出”加入第 i 件物品“的决策时,需要的是绝无加入第 i 件物品的前一状态 dp[i-1][j-w[i]] 。
那么完全背包里,如果内循环是顺序的,那么 max 中的 dp[j] 、dp[j-w[i]] 就是当前状态的值。然而,为什么要让这两个值是当前状态的值呢?
原因很简单,因为完全背包的特性是每种物品可选无限次。所以在做出”多加入一件第 i 种物品“的决策时,需要的是可能已经加入第 i 种物品的当前状态 dp[i][j-w[i]] 。
弄懂了一维的完全背包状态转移方程后,就可以直接拿它做题了, 在本题中把 max 换成 min 即可。
还有一个坑点就是关于状态的初始化,这个分为背包必须填满/背包无须填满的情况,后面一种情况把状态都初始化为 0 即可,因为此时背包无需填满,解可以是 0 ,即什么都不装。而前面一种情况比较特殊,除了 dp[0] 可以是 0 ,其他 dp[1..V] 应该初始为 负无穷(求最大值)或正无穷(求最小值),因为此时就只有空间是 0 的背包能被价值为 0 的 "nothing" 装满,而其他容量的背包都没有合法解,属于未定义的状态,那么它们的解就应该是无穷。
我的AC代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<algorithm> using namespace std; #define INT_MAX 25000005 const int MAX_TAG_V = 9999; const int MAX_CUR_N = 500; const int MAX_CUR_VAL = 10000; int tag_V; int p[MAX_CUR_N]; int w[MAX_CUR_N]; int dp[MAX_TAG_V]; int main(void) { int tag_num; cin >> tag_num; while (tag_num--) { int low_tag_V, upper_tag_V; cin >> low_tag_V >> upper_tag_V; tag_V = upper_tag_V - low_tag_V; int N; cin >> N; //初始化 p, w, dp for (int i = 1; i <= N; ++i) { cin >> p[i] >> w[i]; } dp[0] = 0; for (int i = 1; i <= tag_V; i++) { dp[i] = INT_MAX; //恰好填满,除0外的其他状态应初始化为无解 } //根据状态转移方程求前i种物品、空间为j的背包内最小价值之和 for (int i = 1; i <= N; i++) { for (int j = w[i]; j <= tag_V; j++) { dp[j] = std::min(dp[j], dp[j-w[i]] + p[i]); } } if (dp[tag_V] == INT_MAX) { cout << "This is impossible." << endl; } else { cout << "The minimum amount of money in the piggy-bank is " << dp[tag_V] << "." << endl; } } return 0; }
顺手把求解最大值给实现了,但是这里的包不要求填满,而不像题目中说的必须填满。
说一个简单而有效的优化。如果背包不要求填满的话,对于完全背包问题我们在输入上可以进行优化,因为不是所有种类的物品都会被加入背包,而是那些价格高且重量轻的物品才会被加入背包。用符号描述就是:若两件物品 i、j 满足 value[i] >= value[j] and weight[i] <= weight[j] ,那么在输入时,就可以物品 j 去掉,这种办法可以大大降低物品的件数。
下面的代码就实现了这个优化,可以看看:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<algorithm> #include<cstring> #include<vector> using namespace std; const int MAX_TAG_V = 9999; const int MAX_CUR_N = 500; const int MAX_CUR_VAL = 10000; int tag_v; int p[MAX_CUR_N] = {0}; int w[MAX_CUR_N] = {0}; int dp[MAX_TAG_V] = {0}; struct Thing { int val; int weight; }; bool highVal_lowWeight (const Thing& a, const Thing& b) { if (a.val == b.val) { return a.weight < b.weight; } else{ return a.val > b.val; } } bool greater_than_tagV (const Thing& x) { if (x.weight > tag_v) { return true; } return false; //原默认返回true! } int main(void) { int tag_num; cin >> tag_num; while (tag_num--) { memset(p, 0, sizeof(p)); memset(w, 0, sizeof(w)); memset(dp, 0, sizeof(dp)); int low_tag_v, upper_tag_v; cin >> low_tag_v >> upper_tag_v; tag_v = upper_tag_v - low_tag_v; //优化输入的物品数据 int N; cin >> N; vector<Thing> vec; for (int i = 1; i <= N; ++i ) { Thing temp; cin >> temp.val >> temp.weight; vec.push_back(temp); } std::sort(vec.begin(), vec.end(), highVal_lowWeight); vector<Thing>::iterator it = std::find_if(vec.begin(), vec.end(), greater_than_tagV); while (it != vec.end()) { //将重量大于tag_v的物品删除 vec.erase(it); it = std::find_if(vec.begin(), vec.end(), greater_than_tagV); } int min = vec[0].weight; //只把价值高且重量小的放入背包 N = 1; p[1] = vec[0].val; w[1] = vec[0].weight; for (auto it = vec.begin(); it != vec.end(); ++it) { if (it->weight < min) { N++; p[N] = it->val; w[N] = it->weight; min = it->weight; } } vec.clear(); vector<Thing>().swap(vec); //初始化dp for (int i = 1; i <= N; ++i) { dp[i] = 0; //无要求恰好填满,除0外的其他状态都可以是0(空包) } //根据状态转移方程求前i种物品、空间为j的背包内最小价值之和 for (int i = 1; i <= N; i++) { for (int j = w[i]; j <= tag_v; j++) { dp[j] = std::max(dp[j], dp[j-w[i]] + p[i]); } } cout << "The max in the tag is " << dp[tag_v] << "." << endl; } return 0; }
为了以后使用一维数组解完全背包更方便,这里抽象出一个处理完全背包问题的伪代码:
//过程 CompletePack,表示处理一件背包中的物品,两个参数 cost、weight 分别表示这件物品的费用和价值 procedure CompletePack (cost, weight) for j = cost to V dp[j] = max (dp[j], dp[j-cost] + weight) //以后完全背包问题的伪代码可以这么写 for i=1..N CompletePack (c[i], w[i])