动态规划之背包系列

896 阅读4分钟

开篇问题:

有n个物品,物品重量为 weight = {7,2,4,6} 背包限重 g =11

求背包可装入最大重量。

整个求解过程分为 n 个阶段,每一个阶段决定一个物品是否放入背包中。每个阶段处理完之后(放了或者没放),背包的总重量会有不同的结果。

用一个 二维数组 boolean[][] states[i][j] 来表示 第i个阶段时,背包总重量为j时,true 和 false表示是否可以这样装入。0<= i < n and 0 <= j <=g  

第0阶段(下标为0)进行决策时,重量为7,有两种决策结果,要么放入,要么不放,对应的背包的重量就有两种状态,0或7,都比11 小 对应的 状态表达为 states[0][0] = true, states[0][7] = true

  第1阶段 (下标为1)进行决策时,重量为2,基于上一阶段的决策结果,这个物品决策完了以后,背包有四种状态,前两种为保持原有的背包状态,不加入 0(0+0),7(7+0),后两种为加入 9(7+2),2(0+2),都比11小,对应的状态表达为 states[1][0] = true ,states[1][7] = true,states[1][9] = true 和 states[1][2] = true

第2阶段 (下标为2)进行决策时,重量为4,这个物品决策完了以后,背包有八种状态,前四种为保持原有的背包状态,不加入0(0+0+0),7(7+0+0),9(7+2+0),2(0+2+0),后四种为加入背包中,4(0+0+4),11(7+0+4),13(7+2+4),6(0+2+4),其中13比11大,超重了,忽略。所以其对应的状态表达为 states[2][0] = true ,states[2][7] = true,states[2][9] = true ,states[2][2] = true ,states[2][4] = true ,states[2][11] = true,states[2][6] = true

接下来计算,第3阶段,以此类推,有16种情况,其中超重的忽略,其他的记录状态。

最后只需要在最后一个阶段,即第三阶段,遍历找到一个j最大,且states[i][j] = true ,就是背包能装的最大的重量了。

画个图推理如下:

image.png

 

 

画状态表时,每个阶段填入有个很明显的特征,把上一个阶段的状态先抄过来,然后再把自己的重量在上一个阶段的基础上,加入背包。

即可推导 状态转移方程为

states[i][j] ===>> { states[i-1][j] || states[i-1][j-weight[i] }

 

代码解释:

public int dpackage(int[] weight, int n, int g) {
    boolean[][] states = new boolean[n][g+1]; 
    // 默认值false
    // 第一行的数据要特殊处理,可以利用哨兵优化
    states[0][0] = true;
    if (weight[0] <= g) {
        states[0][weight[0]] = true;
    }
    for (int i = 1; i < n; ++i) { 
    // 动态规划状态转移
        int tmpW = weight[i];
        for (int j = 0; j <= g; ++j) {
            // 不把第i个物品放入背包
            if (states[i - 1][j]) states[i][j] = states[i][j] || states[i-1][j];
            //把第i个物品放入背包
            if(j + tmpW <= g && states[i-1][j]){
                states[i][j+tmpW] = true;
            }
        }
    }
    //最后一层,找最大值
    for (int i = g; i >= 0; --i) { 
        if (states[n - 1][i]) return i;
    }
    return 0;
}

 

这里借机介绍动态规划的三个特性

动态规划三个特性

  1. 最优子结构

问题的最优解包含子问题的最优解,后面阶段的状态可以通过前面推导出来。

每个阶段的最优解在解答时,都已经求出来了,后面的阶段决策需要在前面的结果基础上, 再进行决策。

  1. 无后效性

不关心前面如何推导出来的,现阶段状态一旦确定,就不受之后的决策影响。

  1. 重复子问题

不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。

这个应该用递归解答对比最能理解了,会有多次的重复计算,而动态规划记录了每个阶段决策结果,不会重复计算。

进阶题型:

1 判断具体某个总重量能不能存在背包里

举例:leetcode 416 分割数组

leetcode-cn.com/problems/pa…

target = sum /2 判断target 是否放的进去

和 开头题型 场景 的区别为,最后直接返回最后一个决策阶段, 背包重量为target的状态即可, states[n-1][target] 

int len = nums.length;
if (len == 0) {
    return false;
}
int sum = 0;
for (int num : nums) {
    sum += num;
}
// 特判:如果是奇数,就不符合要求
if ((sum & 1) == 1) {
    return false;
}
int g = sum / 2;
// 创建二维状态数组,行:物品索引,列:容量(包括 0)
boolean[][] states = new boolean[len][g + 1];
// 第一行的数据要特殊处理,可以利用哨兵优化
states[0][0] = true;
// 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
if (nums[0] <= g) {
    states[0][nums[0]] = true;
}
// 再填表格后面几行
for (int i = 1; i < len; i++) {
    for (int j = 0; j <= g; j++) {
        // 不把第i个物品放入背包
        if (states[i - 1][j]) states[i][j] = states[i][j] || states[i-1][j];
        //把第i个物品放入背包
        if(j + nums[i] <= g && states[i-1][j]){
            states[i][j+nums[i]] = true;
        }
    }
}


//最后一个决策阶段,背包重量为g
return states[len - 1][g];

 

2 每个物品有价值,求最大价值

题解:第 开头 题型 的基础上加个价值变量,要求最后求到背包的最大价值。

唯一不同的是,states[i][j] 这里二维数组存储的值不再是 boolean 类型,而是当前决策阶段,背包重量为 j 时,对应的最大总价值。

结果为求最大价值时,遍历 n -1 最后一层。

public static int knapsack3(int[] weight, int[] value, int n, int w) {
    int[][] states = new int[n][w + 1];
    for (int i = 0; i < n; ++i) { 
        // 初始化states
        for (int j = 0; j < w + 1; ++j) {
            states[i][j] = -1;
        }
    }
    states[0][0] = 0;
    if (weight[0] <= w) {
        states[0][weight[0]] = value[0];
    }
    for (int i = 1; i < n; ++i) {
        //动态规划,状态转移
        for (int j = 0; j <= w; ++j) {
            
            if (states[i - 1][j] >= 0) {
                // 不选择第i个物品
                states[i][j] = Math.max(states[i][j], states[i - 1][j]);
                // 选择第 i 个物品
                if (j + weight[i] <= w) {
                    states[i][j + weight[i]] = states[i - 1][j] + value[i];
                }
            }
        }
    }
    
    // 找出最大值
    int maxvalue = -1;
    for (int j = 0; j <= w; ++j) {
        if (states[n - 1][j] > maxvalue) maxvalue = states[n - 1][j];
    }
    return maxvalue;
}

 

3 求某个重量时刻的最大价值

第2种题型的基础上,求某种 重量的 最大价值。 leetcode 494 目标和 与这种情况比较类似,只不过二维数组存的是组合数量,不是价值最大值。感兴趣的同学可以看看。

leetcode-cn.com/problems/ta…

4 其他变形

在 0-1 背包的基础上,求 组合数,最小使用物品个数等,都是可以在以上的思路上,改变二维数组类型,进行思考的。

5 完全背包问题

物品可重复多次放入背包。

leetcode 322 零钱兑换  leetcode-cn.com/problems/co…

leetcode 518 零钱兑换组合数  leetcode-cn.com/problems/co…

这个状态转移方程 和 0-1 背包不同,它不仅依赖于上一决策阶段的结果,还依赖于 同 一阶段背包重量为的 j - weight[i] 状态。

状态转移式: states[i][j] ===>> states[i-1][j] ,states[i][j-weight[i]] 

 

322 零钱兑换

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[][] dp = new int[coins.length][amount+1];
        Arrays.sort(coins);
        for(int k=1;k<=amount;k++){
            dp[0][k] = Integer.MAX_VALUE;
            //初始化
            if(coins[0] <= k && dp[0][k-coins[0]] != Integer.MAX_VALUE){
                dp[0][k] = dp[0][k-coins[0]] +1;
            }
        }
        
        for(int i=1;i<coins.length;i++){
            for(int j=1;j<=amount;j++){
                int temp = Integer.MAX_VALUE;
                if(coins[i] <= j && dp[i][j-coins[i]] != Integer.MAX_VALUE){
                   temp = dp[i][j-coins[i]] +1;
                }
                dp[i][j] = Math.min(temp,dp[i-1][j]);
            }
        }
        return dp[coins.length-1][amount] == Integer.MAX_VALUE ? -1 : dp[coins.length - 1][amount];
    }
}

 

总结

背包系列从简到难,一个系列一步一步添加条件,实际leetcode里的动态规划题目很多都是从中衍生来的。如果能掌握这一系列,脑海中会建立相应的逻辑模型。

个人平时解题习惯

一 回溯 -> 状态转移表法 

平时遇到比较刁钻的动态规划的题目,会试着先写出递归回溯思路代码,感觉这个是较容易被自己笨笨的脑袋接受的,再导出动态规划的状态转移方程

二 领域建模,抽象思维。

理解题意是个很重要的事情,领域建模,转换成一种能理解或者熟悉的思路。抽离出来,化解题意,可以举一反三,事半功倍。

有的时候问题的答案角度比较难想到。所谓站的角度不同,看到的风景就不同,当然思路也会不一样。但平时可以多看看,多学习学习,拓宽思路,解题时候才好泉思喷涌呀。😊