动态规划解题模版:背包型

动态规划:背包型

数组开n+1,背包关键就是看最后一步。

1、LintCode 92: Backpack

【问题】在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]

【分析】从最后一步出发,最后一个物品放还是不放。有两种情况

  • 前n-1个物品能拼出重量 w,那么n个物品也能拼出重量w
  • 前n-1个物品能拼出重量 w - A[n-1],再加上最后一个物品 A[n-1] 拼出w

【状态转移】

  • dp[i][w]表示能否用前i个物品拼出重点w,可以用int数组,也可以用boolean数组,
  • dp[i][j] = max{dp[i-1][j], dp[i-1][j - A[n-1]] + A[i-1]}(int数组)放还是不放
  • dp[i][j] = dp[i-1][w] or dp[i-1][j - A[i-1]](boolean数组)
//int型写法
public static int backPack2(int m, int[] A) {
        int n = A.length;
        int[][] dp = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                //不放
                dp[i][j] = dp[i - 1][j];
                //放
                if (j >= A[i - 1]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - A[i - 1]] + A[i - 1]);
                }
            }
        }
        return dp[n][m];
    }
//boolean型写法
public int backPack(int m, int[] A) {
        int n = A.length;   //物品个数
        boolean[][] dp = new boolean[n + 1][m + 1];
        dp[0][0] = true;
        for (int i = 1; i <= m; i++) {
            dp[0][i] = false;
        }

        for (int i = 1; i <= n; i++) {
            //首先初始化dp[i][0]
            for (int j = 0; j <= m; j++) {
                //不放
                dp[i][j] = dp[i - 1][j];
                //放
                if (j >= A[i - 1]) {
                    // |=,只要有一个为true就是true
                    // 放入A[i-1]的情况就是看j-A[i-1]这个容量下是不是为true,如果为true,那么就是dp[i][j]为true,否则就是看dp[i-1][j]是否为true
                    dp[i][j] |= dp[i - 1][j - A[i - 1]];
                }
            }
        }
        int res = 0;
        for (int i = m; i >= 0; i--) {
            if (dp[n][i] == true) {
                res = i;
                break;
            }
        }
        return res;
    }

优化成一维

public int backPack(int m, int[] A) {
        int f[] = new int[m + 1];
  
        for (int i = 0; i < A.length; i++) {
            for (int j = m; j >= A[i]; j--) {
              	//不放和放,不放就是它自身,
                f[j] = Math.max(f[j], f[j - A[i]] + A[i]);
            } 
        }
        return f[m];
    }

2、LintCode 563: Backpack V

【问题】给出 n 个物品, 以及一个数组, nums[i] 代表第i个物品的大小, 保证大小均为正数, 正整数 target 表示背包的大小, 找到能填满背包的方案数。注意:每一个物品只能使用一次。

【分析】需要求出有多少种组合能组合成target,对于最后一个物品,有放和不放两种选择。

  • 第一种:使用前n-1个物品拼出target
  • 第二种:前n-1个物品能拼出target - nums[i],再加上nums[i],拼出target
  • 拼出target的方式 = 不放+放,即dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i-1]]
  • 如果知道有多少种方式拼出0、1、2…对于有多少种方式拼出target也就知道答案了。

常规写法,时间复杂度O(n*Target)

public static int backPackV1(int[] nums, int target) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }

        int[][] dp = new int[n + 1][target + 1];    //dp[i][j]表示前i个数字有多少种方式拼出数字j
        dp[0][0] = 1;   //0个物品有一种方式拼出重量0
        //初始化
        for (int i = 1; i <= target; i++) {
            dp[0][i] = 0;
        }
        for (int i = 1; i <= n; i++) {
            //拼出几
            for (int j = 0; j <= target; j++) {
                //不放
                dp[i][j] = dp[i - 1][j];
                //放
                if (j >= nums[i - 1]) {     
                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
                }
            }
        }
        return dp[n][target];
    }

第一步优化:利用滚动数组

public static int backPackV3(int[] nums, int target) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int[][] dp = new int[2][target + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= target; i++) {
            dp[0][i] = 0;
        }
        int old = 0, now = 0;
        for (int i = 1; i <= n; i++) {
            old = now;;
            now = 1 - now;
            for (int j = 0; j <= target; j++) {
                //不放
                dp[now][j] = dp[old][j];
                //放
                if (j >= nums[i - 1]) {
                    dp[now][j] += dp[old][j - nums[i - 1]];
                }
            }
        }
        return dp[now][target];
    }

第二步优化:优化成一行。原本是 老值 + 老值 = 新值,如果正着更新,可能会出现 老值 + 新值,所以需要倒着更新

dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i-1]],新值 = 两个老值加起来

public static int backPackV2(int[] nums, int target) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int[] dp = new int[target + 1]; //和总称重有关
        //init:相当于dp[0][0] = 1
        dp[0] = 1;
        //init:dp[0][1] = dp[0][2] = ... = 0
        for (int i = 1; i <= target; i++) {
            dp[i] = 0;
        }

        for (int i = 1; i <= n; i++) {
            //reverse
            for (int j = target; j >= 0; j--) {
                if (j >= nums[i - 1]) {
                    //old + old ==> new old1 = dp[j],old2 = dp[j - nums[i - 1]],new就是直接覆盖
                    dp[j] += dp[j - nums[i - 1]];
                }
            }
        }
        return dp[target];
    }

3、LintCode 564: Backpack VI

【问题】给出一个都是正整数的数组 nums,其中没有重复的数。从中找出所有的和为 target 的组合个数。注意一个数可以在组合中出现多次,数的顺序不同则会被认为是不同的组合。

【分析】这个题和Backpack V的区别是每个物品可以使用多次,且组合中数字可以按照不同顺序,比如1+1+2与1+2+1算是两种情况,这就导致不能按照物品顺序来处理。依旧是关注最后一步,最后一步物品重量是K,那么前面物品构成重量target-K,需要关注最后一个加进来的是谁。

  • 如果最后一个物品重量是A0, 则要求有多少种组合能拼成 Target – A0
  • 如果最后一个物品重量是A1, 则要求有多少种组合能拼成 Target – A1
  • 如果最后一个物品重量是An-1, 则要求有多少种组合能拼成 Target – An-1

【状态转移方程】dp[i]代表有多少种组合能拼出重量i,则dp[i] = dp[i-A[0]] + dp[i-A[1]] +...+ dp[i-A[n-1]]

【初始条件】dp[0] = 1,有1种组合能拼出0

public int backPackVI(int[] nums, int target) {
        int n = nums.length;
        int[] dp = new int[target + 1];
        dp[0] = 1;

        //对于能拼出的i
        for (int i = 1; i <= target; i++) {
            //初始化能拼出i的情况为0种
            dp[i] = 0;  
            //遍历所有数字
            for (int j = 0; j < n; j++) {
                if (i >= nums[j]) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }

4、LintCode 125: Backpack II

【问题】有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值。问最多能装入背包的总价值是多大?注意:每个物品只能取一次,物品不能切分。

public int backPackII(int m, int[] A, int[] V) {
        int n = A.length;
        int[][] dp = new int[n + 1][m + 1];
        //初始化
        for (int i = 0; i <= m; i++) {
            dp[0][i] = 0;
        }
        int res = Integer.MIN_VALUE;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                //不放
                dp[i][j] = dp[i - 1][j];
              	//放
                if (j >= A[i - 1]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - A[i - 1]] + V[i - 1]);
                }
                res = Math.max(res, dp[i][j]);
            }
        }
        return res;
    }

5、LintCode 440: Backpack III

【问题】将Backpack II的物品改为无穷多个,背包最大承重m,求能带走的最大价值。

  • 输入:4个物品,重量为2, 3, 5, 7,价值为1, 5, 2, 4. 背包最大承重是10
  • 输出:15

【分析】Ai-1有无穷多个,可以用1个、2个…在这里可以把物品变为种类,这边状态转移方程变为

  • f[i] [w] = maxk>=0{f[i-1] [w-kAi-1] + kVi-1},表示用前i种物品拼出重量w时的最大总价值,等于用前i-1种物品拼出重量w-kAi-1 时最大总价值,加上k个第i种物品,当k = 0和1时,就可以直接用在Backpack II 中了。

把这个式子展开,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCecL0xb-1575735182775)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191207222836105.png)]

可以优化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-shXqPs9c-1575735182776)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191207223019634.png)]

什么意思呢

假设Ai-1 = 2,Vi-1 = x

f[i] [5] = max{ f[i-1] [5], f[i-1] [3] + x, f[i-1] [1] + 2x }

f[i] [7] = max{ f[i-1] [7], f[i-1] [5] + x, f[i-1] [3] + 2x, f[i-1] [1] + 3x }

这样算了重合的部分,不是我们想要的

// 只需要把Backpack II中关键一行改为
// dp[i][j] = Math.max(dp[i][j],dp[i][j - A[i-1]] + V[i-1])
  
  public int backPackII(int m, int[] A, int[] V) {
        int n = A.length;
        int[][] dp = new int[n + 1][m + 1];
        //初始化
        for (int i = 0; i <= m; i++) {
            dp[0][i] = 0;
        }
        int res = Integer.MIN_VALUE;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                //不放
                dp[i][j] = dp[i - 1][j];
              	//放
                if (j >= A[i - 1]) {
                    dp[i][j] = Math.max(dp[i][j],dp[i][j - A[i-1]] + V[i-1]);
                }
                res = Math.max(res, dp[i][j]);
            }
        }
        return res;
    }

可以优化到一维,这边的细节是:用的不是两个old值,而是old+new,可以只开一个数组,old+new去覆盖本来的old,这就需要时从前往后来,而不是逆序,如图

在这里插入图片描述

public int backPackIII(int m, int[] A, int[] V) {
        int n = A.length;
        int[] dp = new int[m + 1];
        dp[0] = 0;
        int res = Integer.MIN_VALUE;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                if (j >= A[i - 1]) {
                    //old = dp[j],new = dp[j - A[i - 1] + V[i - 1]],加起来覆盖本来的old
                    //old相当于原来的dp[i-1][j],dp[j - A[i - 1]相当于dp[i][j - A[i - 1]
                    dp[j] = Math.max(dp[j], dp[j - A[i - 1] + V[i - 1]]);
                }
                res = Math.max(res, dp[j]);
            }
        }
        return res;
    }
发布了43 篇原创文章 · 获赞 6 · 访问量 3907

猜你喜欢

转载自blog.csdn.net/weixin_44424668/article/details/104017370