zoukankan      html  css  js  c++  java
  • 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])

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

    延伸阅读


      背包九讲

      动态规划之滚动数组  

      动态规划的解题步骤:

      

    ————全心全意投入,拒绝画地为牢
  • 相关阅读:
    mac redis 安装及基本设置 python操作redis
    mac webstorm自动编译typescript配置
    MySQL数据库的基本操作
    python 面试基础考试题收集
    pyhon 列表的增删改查
    python 文件读取方法详解
    MAC下绕开百度网盘限速下载的方法,三步操作永久生效
    浏览器窗口输入网址后发生的一段事情(http完整请求)
    CMDB
    django适当进阶篇
  • 原文地址:https://www.cnblogs.com/Bw98blogs/p/8393992.html
Copyright © 2011-2022 走看看