Fork me on GitHub

动态规划题解

1. DP专题

适用问题

回溯适用的问题:穷举;

动态规划:回溯+存在重复子问题;

案例1 0-1背包问题

0-1背包问题:对于一组不同重量的物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少?

物品重量:{2, 2, 4, 6, 3} 

背包最大承重: 9 

这个问题可以用回溯来解决,回溯的关键是要构建 多阶段决策模型:每一阶段决策一个物品是否装入背包 

  n个物品对应n个阶段,每个阶段决定1个物品是否要装入背包,这组决策对应物品重量是否是最大值,对比下看看背包的物品是否是最大值;

复制代码
    //1.回溯解法
    private int maxW = Integer.MIN_VALUE; //存储背包中物品总重量的最大值,结果放到maxW中
    private int[] weight = {2,2,4,6,3}; //物品重量
    private int n = 5; //物品个数
    private int w = 9; //背包承受的最大重量
    private void f(int i, int cw) { //调用f(0,0),i表示决策的物品,cw前面决策完成之后背包的物品重量; (0, i-1)
        if (cw == w || i == n) { //中间决策的可行性解;
            if (cw > maxW) maxW = cw;
            return;
        }
        f(i+1,cw);//选择不装第i个物品
        if (cw + weight[i] <= w) {
            f(i+1,cw+weight[i]); //选择装第i个物品
        }
    }
复制代码

 递归树 != 决策树 

在决策第二个物品时(重量为2)它的状态有重复的 f(2, 2)、f(2, 2) ,没必要这两个都往下递归,保留一个往下递归即可;其他同理;类似斐波那契数列的重复子问题;

 采用备忘录的方法去解决重复子问题;

定义一个二维数组mem: (n, w+1) 即 (5个物品, 背包重量w+1) 

从性能上来说备忘录的解法和动态规划的一样的了,但是不是所有的问题都可以用备忘录的解法去去重;

复制代码
    //2.回溯解法: 使用备忘录解决重复子问题
    private boolean[][] mem = new boolean[5][10]; //备忘录,默认值false
    public void f_2(int i, int cw) { //调用f_2(0,0)
        if(cw == w || i == n) { //cw==w表示装满了,i==n表示物品都考察完了
            if (cw > maxW) maxW = cw;
            return;
        }
        if (mem[i][cw]) return; //重复状态
        mem[i][cw] = true; //记录(i,cw)这个状态
        f_2(i+1, cw); //选择不装第i个物品
        if (cw + weight[i] <= w) {
            f_2(i+1, cw+weight[i]); //选择装第i个物品
        }
    }
复制代码

动态规划解法

构建多阶段决策模型。把每一层(每一阶段)重复的状态合并,只记录不同的状态,然后基于上一层(上一阶段)的状态集合,来推导下一层(下一阶段)的状态集合。

(i, cw) 表示一个状态,表示第i 个物品决策完之后背包重量为cw,cw的取值为0~w(w表示背包的承载重量) 。

通过合并每一层重复的状态,这样就能保证每一层的状态个数不会超过w个(w表示背包的承载重量),也就是例子中的9;

于是我们就可以避免回溯算法递归树中每层状态个数的指数级增长。

物品重量:{2, 2, 4, 6, 3} 

背包最大承重: 9 

f(i, cw) 表示第i 个物品将要决策,此时背包重量cw;

dp[i][cw] 表示第i个物品决策完,此时背包重量cw; 

 

2  0  2

2  0  2  4

4  0  2  4  6  8

6  0  2  4  6  8

3  0  2  3  4  6  7  8  9

如何记录每一层不重复的状态,可以用hashset,或者一一映射的布尔类型的数组;

 

物品总重量不会超过w,我们用一个boolean类型的二维数组dp[n][w+1]来记录每层可以达到的不同状态。

第0个物品的重量是2,要么装入背包,要么不装入背包,决策完之后,对应背包的状态有2种: 背包中物品的总重量是0或者2; 

我们用dp[0][0]=true和 dp[0][2]=true来表示这两种状态;

第1个物品的重量也是2,在第0个物品决策完之后,背包中物品的重量有2种情况:0或者2;基于第0个物品决策完之后的状态,在第1个物品决策完之后,对应背包中物品总重量分别有三种情况:

0(0+0),2(0+2  or  2+0),4(2+2) ;我们用dp[1][0] = true,dp[1][2] = true, dp[1][4] = true来表示这三种状态。

以此类推,基于第i个物品决策完之后的状态来推导第i+1个物品决策完之后的状态。

2  0  2

2  0  2  4   (0 2 2 4)

4  0  2  4  6  8

6  0  2  4  6  8

3  0  2  3  4  6  7  8  9

对应的dp二维数组见下,状态转移方程,基于上层来填写下一层:

  0 1 2 3 4 5 6 7 8 9
0 1 0 1 0 0 0 0 0 0 0
1 1 0 1 0 1 0 0 0 0 0
2 1 0 1 0 1 0 1 0 1 0
3 1 0 1 0 1 0 1 0 1 0
4 1 0 1 1 1 0 1 1 1 1
复制代码
    //3.动态规划解法
    public int knapsack(int[] weight, int n, int w) {
        boolean[][] dp = new boolean[n][w+1]; //默认值false  dp[n][w+1]记录每层达到的不同状态
        dp[0][0] = true;
        //初始化  weight{2,2,4,6,3}
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;//第0个物品的重量是2,要么装入背包要么不装,对应的背包状态有2种: 0或者2, 即dp[0][0]=true, dp[0][2]=true
        }                           //第1个物品的重量是2,要么装入背包要么不装,对应的背包状态有3种: 0/2/4, 即dp[1][0]=true, dp[1][2]=true, dp[1][4]=true
        for (int i = 1; i < n; i++) {             //动态规划状态转移
            for (int j = 0; j <= w; j++) {        //遍历第i-1个阶段的所有可达状态
                if (dp[i-1][j] == true) {         //判断i-1层状态是否可达,基于上层状态可达推导下一个阶段状态
                    dp[i][j] = true;              //推导第i层状态
                    if (j+weight[i] <= w) {
                        dp[i][j+weight[i]] = true; //推导第i层状态
                    }
                }
            }
        }
        for (int i = w; i >= 0; i--) { //输出结果,把dp填完之后,把为true的从后往前扫描一遍,输出可达的最大背包重量(即最后一个);
            if (dp[n-1][i] == true) return i;
        }
        return 0;
    }
复制代码

案例2 二维费用背包问题

对于一组不同重量、不同价值、不可分割的物品,选择将其中某些物品转入背包,在不超过背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少?

注意这里是求最大总价值,而非最大总重量。

物品重量: {2, 2, 4, 6, 3}

物品价值: {3, 4, 8, 9, 6}

背包最大承重: 9 

复制代码
    //1.回溯法,无法用备忘录去实现  i,cw相同的情况下取max(cv)
    private int maxW = Integer.MIN_VALUE; //存储背包中物品总重量的最大值,结果放到maxW中
    private int[] weight = {2,2,4,6,3}; //物品重量
    private int[] value = {3,4,8,9,6};
    private int n = 5; //物品个数
    private int w = 9; //背包承受的最大重量
    private void f(int i, int cw, int cv) { //调用f(0,0,0), i表示阶段、 cw表可达的物品总重量、 cv表可达的物品总价值
        if (cw == w || i == n) { //cw==w表示装满了,i==n表示物品都考察完了
            if (cv > maxW) maxW = cv;
            return;
        }
        f(i+1,cw,cv);//选择不装第i个物品
        if (cw + weight[i] <= w) {
            f(i+1,cw+weight[i], cv+value[i]); //选择装第i个物品
        }
    }
复制代码

这类就无法使用备忘录去重了,因为相同重量它的总价值可能是不一样的,无法使用mem[n][cw] 去记录;

在第2个物品时,虽然没有重复的,但是f(2, 2, 4) 的总价值 大于 f(2, 2, 3)的总价值,就没必要保留它这个状态往下递归了;

 

动态规划的解法

多阶段决策模型,每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态。

(i, cw, cv) 表示一个状态,第i 个物品决策完之后,背包重量为cw,对应的最大价值为cv;

用一个二维数组dp[n][w+1] ,来记录每个阶段可以达到的状态。dp[ i ][ j ]表示第i 个物品决策完之后,背包重量为j ,对应的最大价值。

把递归树每一层中(i, cw)重复的状态(节点)合并,只记录cv值最大的那个状态,然后基于这些状态来推导下一层(下一个阶段)的状态。 

  0 1 2 3 4 5 6 7 8 9
0 0 -1 3 -1 -1 -1 -1 -1 -1 -1
1 0 -1 4 -1 -1 7 -1 -1 -1 -1
2                    
3                    
4                    

 

复制代码
//2.动态规划解法 {2, 2, 4, 6, 3}, {3, 4, 8, 9, 6}, 5, 9
    //二维数组dp[n][w+1]记录每个阶段可以达到的状态, dp[i][j]表示第i个物品决策完之后,背包重量为j,对应的最大价值
    public int knapsack_2(int[] weight, int[] value, int n, int w) {
        int[][] dp = new int[n][w+1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                dp[i][j] = Integer.MIN_VALUE;//默认最小值,为状态不可达
            }
        }
        dp[0][0] = 0; //dp[0][0] = 0表示状态可达的价值为0, i表示阶段, j表示物品重量cw, dp[i][j]表示最大价值cv
        if (weight[0] <= w) {
            dp[0][weight[0]] = value[0];
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == Integer.MIN_VALUE) { //判断状态是否可达
                    continue;
                }
                dp[i][j] = Math.max(dp[i][j], dp[i-1][j]); //可达(不放进背包), 比较上一个可达状态最大价值和 当前层可达状态最大价值 的大小
                if (j+weight[i] <= w) {
                    dp[i][j+weight[i]] = Math.max(dp[i][j+weight[i]], dp[i-1][j] + value[i]);//可达(放进背包)
                }
            }
        }
        int res = Integer.MIN_VALUE;
        for (int j = 0; j <= w; j++) { //扫描最后一行, 输出结果
            if (res < dp[n-1][j]) {
                res = dp[n-1][j];
            }
        }
        return res;
    }
复制代码

总结

  回溯 + 存在重复子问题/ 重复状态  ==>> 动态规划解决

 

 

解题步骤

  1. 可用回溯解决:需要穷举搜索才能得到结果的问题(最值、可行、计数等);
  2. 构建多阶段决策模型:看是否能将问题求解的过程分为多个阶段;
  3. 查看是否存在重复子问题:是否有多个路径到达同一个状态;
  4. 定义状态:也就是如何记录每一阶段的不重复状态;
  5. 定义状态转移方程:也就是找到如何通过上一阶段的状态推导下一下阶段的状态;
  6. 画状态转移表:辅助理解,验证正确性,确定状态转移的初始值;
  7. 编写动态规划代码;

   4、5两个步骤是难点,掌握的技巧是:记忆经典模型的状态和状态转移方程的定义方法,举一反三。

0-1背包问题:对于一组不同重量的物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少?

  • 1. 可用回溯解决,穷举问题;
  • 2. 构建多阶段决策模型:每一阶段决策一个物品是否放入背包;
  • 3. 查看是否存在重复子问题:某一阶段背包中物品重量为cw,可以通过不同路径到达;
  • 4. 定义状态
    • boolean dp[n][w+1] 记录每一阶段可达的所有状态;
    • dp[i][j] = true表示第i 个物品决策完之后,存在背包中物品重量为j 这种状态;
  • 5. 定义状态转移方程:
    • 确定第i 阶段的(i, j) 这个状态,如何通过上一个阶段i - 1的哪些状态转移过来
    • (i, j)这个状态只有可能由(i-1, j)和 (i-1, j-weight[i]) 转移过来
    • dp[i][j] = dp[i-1][j]  ||  dp[i-1] [j - weight[i]] ;
  • 6. 画状态转移表: 辅助理解,验证正确性,确定状态转移的初始值。
  • 7. 编写动态规划代码
 
复制代码
     public int knapsack2(int[] weight, int n, int w) {
        boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;
        }
        //动态规划状态转移
        //正向推 i -> i+1
        for (int i = 1; i < n; i++) {             //动态规划状态转移
            for (int j = 0; j <= w; j++) {        //遍历第i-1个阶段的所有可达状态
                if (dp[i-1][j] == true) {         //可达
                    dp[i][j] = true;              //推导第i层状态
                    if (j+weight[i] <= w) {
                        dp[i][j+weight[i]] = true; //推导第i层状态
                    }
                }
            }
        }
        //反向推 i-1 -> i ,标准的状态方程写法
        //dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]]
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) {
                    dp[i][j] = true;
                }
            }
        }

        for (int i = w; i >= 0; i--) { //输出结果
            if (dp[n-1][i] == true) return i;
        }
        return 0;
    }
复制代码

二维费用背包: 

对于一组不同重量、不同价值、不可分割的物品,选择将其中某些物品转入背包,在不超过背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少?

  • 1. 可用回溯解决,穷举问题;
  • 2. 构建多阶段决策模型:每一阶段决策一个物品是否放入背包;
  • 3. 查看是否存在重复子问题:某一阶段背包中物品重量为cw,可以通过不同路径到达;
  • 4. 定义状态
    • int dp[n][w+1]  记录每一个阶段可达的所有状态;
    • dp[i][j] 表示第i 个物品决策完之后,存在背包中物品重量为 j ,对应的最大物品价值;
  • 5. 定义状态转移方程:
    • 确定第i 阶段的( i, j ) 这个状态,如何通过上一个阶段 i - 1的哪些状态转移过来;
    • (i, j)这个状态只有可能由(i-1, j)和 (i-1, j-weight[i]) 转移过来;
    • dp[i][j] = Math.max(dp[i-1][j], dp[i-1] [j - weight[i]] + value[i] ) ;
  • 6. 画状态转移表: 辅助理解,验证正确性,确定状态转移的初始值。
  • 7. 编写动态规划代码
 
复制代码
    public int knapsack_22(int[] weight, int[] value, int n, int w) {
        int[][] dp = new int[n][w+1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                dp[i][j] = Integer.MIN_VALUE;
            }
        }
        dp[0][0] = 0;
        if (weight[0] <= w) {
            dp[0][weight[0]] = value[0];
        }
        //正推 i -> i+1
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == Integer.MIN_VALUE) {
                    continue;
                }
                dp[i][j] = Math.max(dp[i][j], dp[i-1][j]);
                if (j+weight[i] <= w) {
                    dp[i][j+weight[i]] = Math.max(dp[i][j+weight[i]], dp[i-1][j] + value[i]);
                }
            }
        }
        //逆推
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] != Integer.MIN_VALUE) {
                    dp[i][j] = Math.max(dp[i][j], dp[i-1][j]);
                }
                if (j-weight[i] >= 0 && dp[i-1][j-weight[i]] != Integer.MIN_VALUE) {
                    dp[i][j] = Math.max(dp[i][j], dp[i-1][j-weight[i]]+ value[i]);
                }
            }
        }

        int res = Integer.MIN_VALUE;
        for (int j = 0; j <= w; j++) { //输出结果
            if (res < dp[n-1][j]) {
                res = dp[n-1][j];
            }
        }
        return res;
    }
复制代码

 

最值、可行、计数三种类型

  一些特殊小类别:树形DP(线性DP和树形DP)、区间DP、数位DP

  • 1. 有n个物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少? (最值)
  • 2. 有n个物品,选择其中一些物品装入背包,能不能正好装满背包?(可行) 
  • 3. 有n个物品,选择其中一些物品装入背包,正好装满背包所需物品最少个数?(如果装不满,返回-1) (最值) 
  • 4. 有n个物品,选择其中一些物品装入背包,装满背包有多少种不同的装法?(计数) 

背包问题都可以对应以上四种问法,0-1、完全、多重和 二维费用;

1、有n个物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少? (最值)

状态:

  • boolean dp[n] [w+1] 记录每阶段可达状态;
  • dp[i] [j] = true表示第i 个物品决策完之后背包重量为 j 这个状态可达;

状态转移方程:

  • (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
  • dp[i][j] = dp[i-1] [j] || dp[i-1][j - weight[i]] 

2. 有n个物品,选择其中一些物品装入背包,能不能正好装满背包 (可行)

状态:

  • boolean dp[n] [w+1] 记录每阶段可达状态;
  • dp[i] [j] = true表示第i 个物品决策完之后背包重量为 j 这个状态可达;

状态转移方程:

  • (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
  • dp[i][j] = dp[i-1] [j] || dp[i-1][j - weight[i]] 

 

复制代码
public int knapsack2(int[] weight, int n, int w) { //可行 
        boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) {
                    dp[i][j] = true;
                }
            }
        }
        return dp[n-1][w];
    }
复制代码

3. 有n个物品,选择其中一些物品装入背包,正好装满背包所需物品最少个数?(如果装不满,返回-1) (可行)

状态:

  • int dp[n] [w+1] 记录每阶段可达重量对应的最少物品个数;
  • dp[i] [j] = true表示第i 个物品决策完之后,背包重量为 j,对应的最少物品个数;

状态转移方程:

  • (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
  • dp[i][j] = Math.min(dp[i-1] [j],  dp[i-1][j - weight[i]] + 1) 
复制代码
    public int knapsack3(int[] weight, int n, int w) { //最值 
        int[][] dp = new int[n][w+1]; //记录到达某个状态,最少物品数量 
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                dp[i][j] = Integer.MAX_VALUE;
            }
        }
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = 1;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) { 
            for (int j = 0; j <= w; j++) {
                if ([j-weight[i]] < 0)) {
                    dp[i][j] = dp[i-1][j];
                } else {
                    dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]] + 1)
                }
            }
        }
        if (dp[n-1][w] == Integer.MAX_VALUE) return -1;
        return dp[n-1][w];
    }
复制代码

4. 有n个物品,选择其中一些物品装入背包,装满背包有多少种不同的装法?(计数)

状态:

  • int dp[n] [w+1] 记录每阶段可达重量对应的装法个数;
  • dp[i] [j] = true表示第i 个物品决策完之后,背包重量为 j,对应有几种装法;

状态转移方程:

  • (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
  • dp[i][j] = dp[i-1] [j] + dp[i-1][j - weight[i]]  
复制代码
public int knapsack4(int[] weight, int n, int w) { //计数 
        int dp = new int[n][w+1]; //记录到达某个状态有几条路径
        dp[0][0] = 1;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = 1;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if ([j-weight[i]] < 0)) {
                      dp[i][j] = dp[i-1][j];
                    } else {
                      dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]];
                }
            }
        }
        return dp[n-1][w];
    }
复制代码

 

空间优化

对于一组不同重量、不可分割的物品,选择其中一些物品装入背包,能不能正好装满背包?

物品重量:{2, 2, 4, 6, 3} 

背包最大承重: 9 

状态:

  • boolean dp[n] [w+1] 记录每阶段可达状态;
  • dp[i] [j] = true表示第i 个物品决策完之后,背包重量为 j 这个状态可达;

状态转移方程:

  • (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
  • dp[i][j] = dp[i-1] [j]  ||  dp[i-1][j - weight[i]]  
  0 1 2 3 4 5 6 7 8 9
0 1 0 1 0 0 0 0 0 0 0
1 1 0 1 0 1 0 0 0 0 0
2
3
4 1

 状态转移只用 i-1 和i 行,滚动的去利用;

  0 1 2 3 4 5 6 7 8 9
0                    
1                    
复制代码
//二维数组 
public int knapsack2(int[] weight, int n, int w) { //可行 
        boolean[][] dp = new boolean[n][w+1]; //默认值false; 
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) {
                    dp[i][j] = true;
                }
            }
        }
        return dp[n-1][w];
    }
    
//滚动数组 
public int knapsack2_mem(int[] weight, int n, int w) { //可行 
        boolean[][] dp = new boolean[2][w+1]; //默认值false; 
        dp[0][0] = true;
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;
        }
        int turn=1; //该填充第turn行了(trun=0或1) 
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[(turn+1)%2][j] == true || (j-weight[i] >= 0 && dp[(turn+1)%2][j-weight[i]] == true)) {
                    dp[turn][j] = true;
                }
            }
            turn = (turn+1)%2; //0变为1, 或者1变为0
        }
        return dp[(turn+1)%2][w];
    }
复制代码

一维数组

dp[i][j]  = dp[i-1][j] || dp[i-1][j - weight[i]] 

复制代码
public int knapsack2_mem(int[] weight, int n, int w) {  
        boolean[][] dp = new boolean[n][w+1]; //默认值false; 
        dp[0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[weight[0]] = true;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = w; j >= 0; j--) {
                if (dp[j] == true || (j-weight[i] >= 0 && dp[j-weight[i]])) {
                    dp[j] = true;
                }
            }
        }
        return dp[w];
    }
复制代码

 

2. 经典模型

背包问题

(0-1、完全、多重、二维费用、分组、有依赖的)

1)0-1背包问题:

有n个物品,重量分别为weight[i] (i=0~n-1)每个物品只有一个,选择其中一些物品装入背包,

在不超过背包总重量w的前提下,...

2)完全背包问题

有n个物品,重量分别为weight[i] (i=0~n-1),每个物品有无限多个,选择一些物品装入背包,

在不超过背包总重量w的前提下...

3)多重背包问题

有n个物品,重量分别为weight[i] (i=0~n-1),每个物品有有限多个,个数分别为count[i] (i=0~n-1),

选择一些物品装入背包, 在不超过背包重量w的前提下...

4)二维费用

有n个物品,重量分别为weight[i] (i=0~n-1), 价值分别为value[i] (i=0~n-1), 在不超过背包重量的前提下,

装入背包物品的最大价值是多少?

要求解的问题:

  • a)背包可装物品总重量的最大值是多少?
  • b)是否能装满整个背包?
  • c)正好装满背包最少需要多少物品?
  • d)装满背包有多少种装法?

把所有解都罗列出来不能用dp来求解,dp只适合最优、计数可行,所有解罗列出适合回溯 如排列组合等; 

0-1背包问题

有n个物品,重量分别为weight[i] (i=0~n-1)每个物品只有一个, 选择其中一些物品装入背包, 在不超过背包总重量w的前提下, ... 

0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个 

状态:

boolean dp[n][w+1] 记录每阶段可达状态;

dp[i][j] = true 表示第i 个物品决策完之后背包重量为j 这个状态可达; 

状态转移方程:

(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来 

dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]]  

要求解的问题:

  • a)背包可装物品总重量的最大值是多少?
复制代码
    public int knapsack1(int[] weight, int n, int w) {
        boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;
        }
        //动态规划状态转移
        //反向推 i-1 -> i ,标准的状态方程写法
        //dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]]
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) {
                    dp[i][j] = true;
                }
            }
        }

        for (int i = w; i >= 0; i--) { //输出结果
            if (dp[n-1][i] == true) return i;
        }
        return 0;
    }
复制代码

 

要求解的问题:

  • b)是否能装满整个背包?

背景和要求解的问题 a)背包可装物品总重量的最大值是多少? 是一样的, 解法是很类似的,只是最后返回的结果不一样;

看最后dp[n-1][w] 是否为true,为true说明是可达的,背包的总重量可以是w,如果为fale 状态不可达 背包总重量不可以达到w;

复制代码
    public int knapsack2(int[] weight, int n, int w) {
        boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = true;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) {
                    dp[i][j] = true;
                }
            }
        }

        return dp[n-1][w];
    }
复制代码

要求解的问题:

  • c)正好装满背包最少需要多少物品?

0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个 

状态:

int dp[n][w+1]  记录每阶段可达重量对应的最少物品个数; 

dp[i][j] = true 表示第i 个物品决策完之后,背包重量为j,对应的最少物品个数; 

状态转移方程:

(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来 

dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]]+1 )   

最值问题, (i, j)不同路径到达的个数不一样;

 

复制代码
 public int knapsack3(int[] weight, int n, int w) { //最值 
        int[][] dp = new int[n][w+1]; //记录到达某个状态,最少物品数量 
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                dp[i][j] = Integer.MAX_VALUE;
            }
        }
        dp[0][0] = true;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = 1;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) { 
            for (int j = 0; j <= w; j++) {
                if ([j-weight[i]] < 0)) {
                    dp[i][j] = dp[i-1][j];
                } else {
                    dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]] + 1) //这里会有越界问题
                }
            }
        }
        if (dp[n-1][w] == Integer.MAX_VALUE) return -1;
        return dp[n-1][w];
    }
复制代码

要求解的问题:

  • d)装满背包有多少种装法?

0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个 

状态:

int dp[n][w+1]  记录每阶段可达重量对应的最少装法个数; 

dp[i][j] 表示第i 个物品决策完之后,背包重量为j,对应有几种装法; 

状态转移方程:

(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来 

dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]   

复制代码
public int knapsack4(int[] weight, int n, int w) { //计数 
        int dp = new int[n][w+1]; //记录到达某个状态有几条路径
        dp[0][0] = 1;
        //初始化
        if (weight[0] <= w) {
            dp[0][weight[0]] = 1;
        }
        //动态规划状态转移
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                if ([j-weight[i]] < 0)) {
                      dp[i][j] = dp[i-1][j];
                    } else {
                      dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]];
                }
            }
        }
        return dp[n-1][w];
    }
复制代码

完全背包问题 

有n个物品,重量分别为weight[i] (i=0~n-1),每个物品有无限多个, 选择一些物品装入背包, 在不超过背包总重量w的前提下...

0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个 

状态:

boolean dp[n][w+1] 记录每阶段可达状态;

dp[i][j] = true 表示第i 个物品决策完之后背包重量为j 这个状态可达; 

状态转移方程:

(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来 

dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]]  

 

类比:

完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个 

状态: 不变

状态转移方程: 

(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;

dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]] || dp[i-1][j-2*weight[i]] || ...|| dp[i-1][j-k*weight[i]]

要求解的问题:

  • a)背包可装物品总重量的最大值是多少?
  • b)是否能装满整个背包?
  • c)正好装满背包最少需要多少物品?
  • d)装满背包有多少种装法?

 

复制代码
    //完全背包-1 : 背包重量w的限制下最大可装物品重量
    public int wanquan_1(int[] weight, int n, int w) {
        boolean[][] dp = new boolean[n][w+1]; //默认false
        for (int c = 0; c <= w/weight[0]; ++c) { //最大装 w/k, w/weight[0]
            dp[0][c*weight[0]] = true;
        }

        for (int i = 1; i < n; ++i) {
            for (int j = 0; j <= w; ++j) { //(i, j)
                int k = j/weight[i];
                for (int c = 0; c <= k; ++c) {
                    if (dp[i-1][j-c*weight[i]]) {
                        dp[i][j] = true;
                        break;
                    }
                }
            }
        }
        for (int i = w; i >= 0; --i) {
            if (dp[n-1][i] == true) return i;
        }
        return 0;
    }
复制代码

要求解的问题:

  • b)是否能装满整个背包?
复制代码
    //完全背包-2 : 是否能装满背包
    public boolean wanquan_2(int[] weight, int n, int w) {
        boolean[][] dp = new boolean[n][w+1]; //默认false
        for (int c = 0; c <= w/weight[0]; ++c) { //最大装 w/k, w/weight[0]
            dp[0][c*weight[0]] = true;
        }

        for (int i = 1; i < n; ++i) {
            for (int j = 0; j <= w; ++j) { //(i, j)
                int k = j/weight[i];
                for (int c = 0; c <= k; ++c) {
                    if (dp[i-1][j-c*weight[i]]) {
                        dp[i][j] = true;
                        break;
                    }
                }
            }
        }
        return dp[n-1][w];
    }
复制代码

2) 完全背包问题 

要求解的问题:

  • c)正好装满背包最少需要多少物品? 

0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个 

状态:

int dp[n][w+1]  记录每阶段可达重量对应的最少物品个数; 

dp[i][j] 表示第i 个物品决策完之后,背包重量为j,对应的最少物品个数; 

状态转移方程:

(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来 

dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]]+1 )   

 

完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个 

状态: 不变

状态转移方程: 

(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;

dp[i][j] =  Math.min( dp[i-1][j], dp[i-1][j-weight[i]]+1, dp[i-1][j-2*weight[i]] + 2,  ..., dp[i-1][j-k*weight[i]] + k )

 
复制代码
    //完全背包-3 : 装满背包所需最少物品个数
    public int wanquan_3(int[] weight, int n, int w) {
        int[][] dp = new int[n][w+1]; //默认false
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j <= w; j++) {
                dp[i][j] = Integer.MAX_VALUE;
            }
        }
        for (int c = 0; c <= w/weight[0]; c++) {
            dp[0][c*weight[0]] = c;
        }

        for (int i = 1; i < n; ++i) {
            for (int j = 0; j <= w; ++j) { //(i, j)
                int k = j/weight[i];
                for (int c = 0; c <= k; ++c) {
                    if (dp[i-1][j-c*weight[i]] != Integer.MAX_VALUE && dp[i-1][j-c*weight[i]] + c < dp[i][j]) {
                        dp[i][j] = dp[i-1][j-c*weight[i]] + c;
                    }
                }
            }
        }
        return dp[n-1][w];
    }
复制代码

 

  • d)装满背包有多少种装法?

0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个 

状态:

int dp[n][w+1]  记录每阶段可达重量对应的装法个数; 

dp[i][j] 表示第i 个物品决策完之后,背包重量为j,对应有几种装法; 

状态转移方程:

(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来 

dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]    

 

完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个 

状态: 不变

状态转移方程: 

(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;

dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]] + dp[i-1][j-2*weight[i]] +  dp[i-1][j-k*weight[i]] 

 
复制代码
    //完全背包-4 : 装满背包总共有多少种装法
    public int wanquan_4(int[] weight, int n, int w) {
        int[][] dp = new int[n][w+1]; //默认false
        for (int c = 0; c <= w/weight[0]; c++) {
            dp[0][c*weight[0]] = 1;
        }

        for (int i = 1; i < n; ++i) {
            for (int j = 0; j <= w; ++j) { //(i, j)
                int k = j/weight[i];
                for (int c = 0; c <= k; ++c) {
                        dp[i][j] += dp[i-1][j-c*weight[i]];
                }
            }
        }
        return dp[n-1][w];
    }
复制代码

多重背包问题 

有n个物品,重量分别为weight[i] (i = 0~n-1) ,每个物品有有限多个,个数分别为count[i] (i = 0~n-1),选择一些物品装入背包,在不超过背包重量w的前提下... 

完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个 

状态: 

boolean dp[n][w+1] 记录每阶段可达状态;

dp[i][j] = true,表示第i 个物品决策完之后背包重量为j 这个状态可达; 

状态转移方程:

(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;

dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]] + dp[i-1][j-2*weight[i]] +  dp[i-1][j-k*weight[i]] 

 

要求解的问题:

  • a)背包可装物品总重量的最大值是多少?
  • b)是否能装满整个背包?
  • c)正好装满背包最少需要多少物品?
  • d)装满背包有多少种装法?

多重背包问题和 完全背包的区别在于, 多重背包多了个 c <= Math.min(w/weight[0], count[0]) 

 

 

 

 

路径问题

 

打家劫舍&股票买卖

 

爬楼梯问题

 

匹配问题(LCS、编辑距离)

 

其他(LIS)

 

 

 

  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
 
  • 添加到短语集
     
    • 没有此单词集:英语 → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:英语 → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
 
 
  • 添加到短语集
     
    • 没有此单词集:中文(简体) → ...
       
    • 创建新的单词集...
  • 拷贝
 
 
posted @   kris12  阅读(124)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
levels of contents
点击右上角即可分享
微信分享提示