HDU #2191 买米问题 深入理解多重背包及其优化
Description
急!灾区的食物依然短缺!
为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品,其价格不等,并且只能整袋购买。
请问:你用有限的资金最多能采购多少公斤粮食呢?
后记:
人生是一个充满了变数的生命过程,天灾、人祸、病痛是我们生命历程中不可预知的威胁。
月有阴晴圆缺,人有旦夕祸福,未来对于我们而言是一个未知数。那么,我们要做的就应该是珍惜现在,感恩生活——
感谢父母,他们给予我们生命,抚养我们成人;
感谢老师,他们授给我们知识,教我们做人
感谢朋友,他们让我们感受到世界的温暖;
感谢对手,他们令我们不断进取、努力。
同样,我们也要感谢痛苦与艰辛带给我们的财富~Input输入数据首先包含一个正整数C,表示有C组测试用例,每组测试用例的第一行是两个整数n和m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的种类,然后是m行数据,每行包含3个数p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应种类大米的袋数。Output对于每组测试数据,请输出能够购买大米的最多重量,你可以假设经费买不光所有的大米,并且经费你可以不用完。每个实例的输出占一行。Sample Input
1 8 2 2 100 4 4 100 2Sample Output
400
思路
这道题其实就是多重背包问题,即有 N 种物品和一个容量为 V 的背包,第 i 种物品最多有 n[i] 件可用,每件费用是 c[i] ,价值是 w[i] ,求哪些物品装入背包可以使得这些物品的费用总和不超过背包容量,且价值总和最大。在这道题中,背包容量是经费,费用是每种米的价格,价值是每种米的重量,求用给定的经费买米,使得买到米的总重量最大,注意经费是可以剩余的。
其实多重背包问题和完全背包问题非常相似,只不过前者的物品是有限件而后者物品是无限件。我们可以把完全背包的状态转移方程改造一下而得到多重背包的状态转移方程,如下:
dp[i][j] = max { dp[i-1][v-k*c[i]] + k*w[i] | 0 <= k <= n[i] }
算法通过穷举 k 而得到所有的解,然后在从其中挑出最大的。我们根据它很容易就可以写出算法,时间复杂度为 O( V·∑n[i] ) :
#include<iostream> #include<algorithm> using namespace std; const int MAX_N = 100; //经费价格上限 const int MAX_M = 100; //大米种类上限 int price[MAX_M+1]; //价格 int weight[MAX_M+1]; //重量 int num[MAX_M+1]; //袋数 int dp[MAX_M+1][MAX_N+1] = {0}; //最大重量 int main(void) { int test_num; cin >> test_num; while (test_num--) { int n, m; //经费金额, 大米种类 cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> price[i] >> weight[i] >> num[i]; } //用j经费买前i种米,能买到的最大总重量 for (int i = 1; i <= m; i++){ for (int j = 1; j <= n; j++) { int max_weight = 0; for (int k = 0; k <= num[i]; k++) { if (j-k*price[i] >= 0) { if (max_weight < dp[i-1][j-k*price[i]] + k*weight[i] ) { max_weight = dp[i-1][j-k*price[i]] + k*weight[i]; } } } dp[i][j] = max_weight; } } cout << dp[m][n] << endl; } return 0; }
然而,它和自顶向下的递归算法一比,时间和空间并没有得到优化,所以我先从空间上去优化这个算法。
由于01背包问题和完全背包问题都有一维数组实现的算法,那么我们可以试着把多重背包问题转化成其中一个问题再进行求解。我先把它转化成好写且好理解的01背包问题。在01背包问题中需要我们以“件”为单位而不是以“种”为单位去看待物品,所以需要把一种物品拆分成多件物品。
如何拆分呢?
直接把第 i 种物品拆分成 n[i] 件就好啦。
额外再说个完全背包问题中拆分物品的方法。如果物品的个数是 N ,背包的容量是 V ,第 i 种物品的费用是 cost[i] ,那么应该把第 i 种物品拆分成 V/cost[i] 件。举个例子,如果物品个数是 3 ,背包的容量是 5 ,那么拆分前的物品序列是下面图左,拆分后的物品序列是下面的图右:
拆分好之后,就直接套用一维滚动数组解决01背包问题的算法即可,空间复杂度优化到为 O(V) :
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int MAX_N = 100; //经费价格上限 const int MAX_M = 100; //大米种类上限 int price[MAX_M+1]; //价格 int weight[MAX_M+1]; //重量 int num[MAX_M+1]; //袋数 int dp[MAX_N+1]; //最大重量 int main(void) { int test_num; cin >> test_num; while (test_num--) { int n, m; //经费金额, 大米种类 cin >> n >> m; for (int i = 1; i <= m; ++i) { cin >> price[i] >> weight[i] >> num[i]; } //初始化dp memset(dp, 0, sizeof(dp)); //用j经费买前i种米,能买到的最大总重量 for (int i = 1; i <= m; i++){ //对第i种米进行num[i]次01选择 for (int k = 1; k <= num[i]; k++) { for (int j = n; j >= price[i]; j--) { //要么选,要么不选 dp[j] = std::max(dp[j], dp[j-price[i]] + weight[i] ); } } } /* cout << "检查中" << endl; for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++){ cout << dp[i][j] << " "; } cout << endl; } */ cout << dp[n] << endl; } return 0; }
但是算法的时间复杂度还是 O( V·∑n[i] ) ,如何优化时间复杂度呢?接着往下看。
拆分时可以采用基于二进制思想的拆分法,因为不管最优解中选了几件第 i 种物品,都可以转化为若干个 2^k 件物品的和。具体的策略是这样:将第 i 种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1, 2, 4, .., 2^(k-1), n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。分成的这几件物品的系数和为 n[i],表明不可能取多于 n[i] 件的第i种物品。
这样拆分的好处是可以把一种物品拆分成 ⌈lgn[i]⌉ 件物品,将原问题转化成了只有 ⌈lgn[i]⌉ 件物品的01背包问题,问题的规模得到指数级的优化,那么运行时间自然就小了。
算法的时间复杂度优化到了 O( V·∑⌈lgn[i]⌉ ) 。
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int MAX_N = 100; //经费价格上限 const int MAX_M = 2000; //拆分后大米单位是件而不是种,需要存储的空间增大 int price[MAX_M+1]; //价格 int weight[MAX_M+1]; //重量 int num[MAX_M+1]; //袋数 int dp[MAX_N+1]; //最大重量 int main(void) { int test_num; cin >> test_num; while (test_num--) { int n, m; //经费金额, 大米种类 cin >> n >> m; //在输入时就进行二进制优化版拆分,相当于增加新物品 int new_m = 0; //拆分后,物品总量为new_m for (int i = 1; i <= m; i++) { int i_price, i_weight, amount; int k = 1; //拆分时每件物品的系数 cin >> i_price >> i_weight >> amount; while (amount > k) { new_m++; price[new_m] = i_price*k; weight[new_m] = i_weight*k; amount = amount - k; //只有相减,amount最后才会等于"n[i]-2^k+1>0"的最大整数 k = k*2; //k是2的指数幂 } new_m++; //cout << "最后一件物品的系数为:" << amount << endl; price[new_m] = i_price*amount; weight[new_m] = i_weight*amount; } /* cout <<"拆分后,每袋米的价格:" << endl; for (int i = 1; i <= new_m; i++) { cout << price[i] << " "; } cout << endl; cout <<"拆分后,每袋米的重量:" << endl; for (int i = 1; i <= new_m; i++) { cout << weight[i] << " "; } cout << endl; */ //初始化dp memset(dp, 0, sizeof(dp)); //用j经费买前i种米,能买到的最大总重量 for (int i = 1; i <= new_m; i++) { //对 new_m 袋大米进行01选择 for (int j = n; j >= price[i]; j--) { dp[j] = std::max(dp[j], dp[j-price[i]] + weight[i] ); } } cout << dp[n] << endl; } return 0; }
下面给出O(log amount)时间处理多重背包中一种物品的过程,其中amount表示物品的数量:
procedure MultiplePack(cost,weight,amount) if cost*amount>=V CompletePack(cost,weight) return integer k=1 while k<num ZeroOnePack(k*cost,k*weight) amount=amount-k k=k*2 ZeroOnePack(amount*cost,amount*weight)