POJ #3624 Charm Bracelet 深入理解01背包以及其优化

Description


 

Bessie has gone to the mall's jewelry store and spies a charm bracelet. Of course, she'd like to fill it with the best charms possible from the N (1 ≤ N ≤ 3,402) available charms. Each charm i in the supplied list has a weight Wi (1 ≤ Wi ≤ 400), a 'desirability' factor Di (1 ≤ Di ≤ 100), and can be used at most once. Bessie can only support a charm bracelet whose weight is no more than M (1 ≤ M ≤ 12,880).

Given that weight limit as a constraint and a list of the charms with their weights and desirability rating, deduce the maximum possible sum of ratings.

Input

* Line 1: Two space-separated integers: N and M
* Lines 2..N+1: Line i+1 describes charm i with two space-separated integers: Wi and Di

Output

* Line 1: A single integer that is the greatest sum of charm desirabilities that can be achieved given the weight constraints

Sample Input

4 6
1 4
2 6
3 12
2 7

Sample Output

23

思路


 

  最基础的背包题目,特点就是:每种物品只有一件,你可以选择放或者不放。

  刚上手的话很容易拿贪心去解这道题,因为贪心和动态规划都有共同的特征,就是问题本身就具有最优子结构,意思是说原问题的最优解是由子问题的最优解演变成的,且子问题的最优解的形成只与某个或某几个子子问题有关,而与另外一个或几个子问题无关。想着是不是可以每次都挑价值最大的物品,如果考虑上容量的话,那就挑单位体积价值最大的物品咯,但是通过下面这个例子你能够很明显地发现这么想是有bug的:

  假设有一个容量为 50 的包与三件重量和价值分别是 10&60、20&100、30&120 的物品,计算知道三件物品单位体积价值分别是 6、5、4 ,采用贪心策略的话,会将前两个物品放入包中,但是很明显将后两个物品放入包中才是最优解。

  为什么会这样呢?我想问题应该出在背包的容量上,贪心不能保证背包的空间恰好被填满,而剩余的容量使总价值降低了。  

  那么采用动态规划解题,DP解题首先思考子状态。显然如果枚举计算前 i 个物品的最大金额的话,所做的决策都会有后效性,即当前的决策使得空间变小了,导致后面价值大的物品装不下。后效性是DP问题定义状态时一定要避免的,所以这个子状态OVER,不能用。

  我们换个子状态(子问题)思考,原问题里的决策是选 or 不选,共 i 个决策(也称 i 个阶段),那么影响下一步决策的因素是背包容量,因为如果背包容量不够了就只能做出不选的决策。所以用背包容量定义子状态。

  枚举背包容量计算最大价值,即 dp[i][j] 表示前 i 件物品放入容量为 j 的背包中可以获得的最大价值,可以得到一个等式,也就是状态转移方程:

dp[i][j] = max (dp[i-1][j], dp[i-1][j-w[i]] + d[i]) 

//d[i] 表示第 i 件物品的价值,w[i]表示第 i 件商品的重量

  

  有了状态转移方程,我们就可以开始解题啦。

  首先想到递归是解决递推式,它是最简单粗暴的办法。但缺点是开大数组,本题会爆内存,我也就不从递归入手了。

  那么动手写一个自底向上的方法,二维数组实现:

#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N = 3402;
const int MAX_M = 12880;
int d[MAX_N+1] = {0};
int w[MAX_N+1] = {0};
int dp[MAX_N+1][MAX_M+1] = {0};

int main(void) {
    int N, M;
    cin >> N >> M;
    for (int i = 1; i <= N; i++) {
        cin >> w[i] >> d[i];
    }
    for (int i = 1; i <= N; i++) { //这一维表示物品
        for (int j = 0; j <= M; j++) { //这一维表示背包容量
            if (w[i] <= j) { 
                dp[i][j] = std::max(dp[i-1][j], dp[i-1][j-w[i]]+d[i]); //前i个物品价值之和,分选i与不选i两种
            }
            else {
                dp[i][j] = dp[i-1][j]; //物品超重,不选i
            }
        }
    }
    cout << dp[N][M] << endl;
    return 0;
}
View Code

 

  虽然算法的时间复杂度是 O(N·M) ,但是空间复杂度是 O(N·M) ,会爆内存。

  如何优化空间复杂度?

  分析发现,第 i 行的数据其实是由第 i-1 行数据计算得到的,也就是说第 i 行的数据只需第 i-1 行的数据就可递推得到而无需第 1 到 i-2 行的数据,那么为什么不让只有两行的数组去存储数据呢?

  我们利用一个变量 c 去实现数据的滚动存储。初始化 c = 0,每次循环前,让 c = 1- c ,实现数组行下标在 0,1 之间循环变化。这种微妙的存储优化方式叫做滚动数组,优化后,空间复杂度为 O(2·M)

#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N = 3402;
const int MAX_M = 12880;
int d[MAX_N+1] = {0};
int w[MAX_N+1] = {0};
int dp[2][MAX_M+1] = {0};

int main(void) {
    int N, M;
    cin >> N >> M;
    for (int i = 1; i <= N; i++) {
        cin >> w[i] >> d[i];
    }
    //二维滚动数组实现DP
    int c = 0;
    for (int i = 1; i <= N; i++) { //这一维表示物品
        c = 1-c;
        for (int j = 0; j <= M; j++) { //这一维表示背包容量
            if (w[i] <= j) { 
                dp[c][j] = std::max(dp[1-c][j], dp[1-c][j-w[i]]+d[i]);
            }
            else {
                dp[c][j] = dp[1-c][j]; //物品超重,不选i
            }
        }
    }
    if (N%2 == 0) { //物品个数为偶数时最优解位于第一行末尾
        cout << dp[0][M] << endl;
    }
    else { //物品个数为奇数时最优解位于第二行的末尾
        cout << dp[1][M] << endl;
    }
    return 0;
}
View Code

 

  其实还可以把二维的滚动数组降到一维,但是要特别注意滚动的方向。

  回顾一下二维数组实现的状态转移方程:

dp[i][j] = max (dp[i-1][j], dp[i-1][j-w[i]] + d[i]) 

  现在用 dp[j] 表示把前 i 件物品放入容量为 j 的背包中得到的价值。它可以表示当前状态 dp[i][j] 。而它是由两个子问题 dp[i-1][j] 、dp[i-1][j - w[i]] 递推而来的。

  那么,如何保证推当前状态 dp[i][j] 时(也就是第 i 次循环推 dp[j] 时),能够得到前一状态 dp[i-1][j] 、dp[i-1][j-w[i]]  的值?

  关键就是让一维滚动数组逆序滚动更新,也就是内循环逆序。 

for i = 1 .. N
    for j = V .. 0
        dp[j] = max (dp[j], dp[j-w[i]] + d[i])

  内循环是逆序时,就可以保证 max 中的 dp[j] 、dp[j-w[i]] 是前一状态的!  

         

  算法的空间复杂度是 O(M)  

#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N = 3402;
const int MAX_M = 12880;
int d[MAX_N+1] = {0};
int w[MAX_N+1] = {0};
int dp[MAX_M+1] = {0};

int main(void) {
    int N, M;
    cin >> N >> M;
    for (int i = 1; i <= N; i++) {
        cin >> w[i] >> d[i];
    }
    for (int i = 1; i <= N; i++) {
        for (int j = M; j >= 1; j--) {
            if (w[i] <= j) {
                dp[j] = std::max(dp[j], dp[j-w[i]] + d[i] );
            }
            else {
                dp[j] = dp[j];
            }
        }
    }
    cout << dp[M] << endl;
    return 0;
}
View Code

  

  再优化,减少无用的内循环循环次数,有:

#include<iostream>
#include<algorithm>
using namespace std;
const int MAX_N = 3402;
const int MAX_M = 12880;
int d[MAX_N+1] = {0};
int w[MAX_N+1] = {0};
int dp[MAX_M+1] = {0};

int main(void) {
    int N, M;
    cin >> N >> M;
    for (int i = 1; i <= N; i++) {
        cin >> w[i] >> d[i];
    }
    for (int i = 1; i <= N; i++) {
        for (int j = M; j >= w[i]; j--) {
            dp[j] = std::max(dp[j], dp[j-w[i]] + d[i] );
        }
    }
    cout << dp[M] << endl;
    return 0;
}
View Code

 

  由于一维数组解 01背包公式以后会常常会被调用,所以这里给出伪代码:

//过程 ZeroOnePack,表示处理一件背包中的物品,两个参数 cost、weight 分别表示这件物品的费用和价值
procedure ZeroOnePack (cost, weight)
    for j = j to cost
        dp[j] = max (dp[j], dp[j-cost] + weight)

//以后01背包问题的伪代码可以这么写
for i=1..N
    ZeroOnePack (c[i], w[i])

 

最后谈谈一维度背包的初始化的问题,直接引用《背包九讲》:

 

 

 

延伸阅读


 

  背包九讲

  动态规划之滚动数组  

  动态规划的解题步骤:

  

 

转载于:https://www.cnblogs.com/Bw98blogs/p/8393992.html

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值