动态规划详解

(注:为黑马教学笔记整理,用以自用,如有条件,请支持正版教学)

起始:不同路径

在这里插入图片描述
(0,0) (0,1)
(1,0) (1,1)
(2,0) (2,1)
如果终点是 (0,1) 那么只有一种走法

如果终点是 (1,0) 那么也只有一种走法

如果终点是 (1,1) 呢,它的走法是从它的上方走下来,或者从它的左边走过来,因此走法 = (0,1) + (1,0) = 2种

如果终点是 (2,0) 那么也只有一种走法

如果终点是 (2,1) 呢,它的走法是从它的上方走下来,或者从它的左边走过来,因此走法 = (1,1) + (2,0) = 3种

总结规律发现:

终点是 (0,1) (0,2) (0,3) … (0,n) 走法只有1种
终点是 (1,0) (2,0) (3,0) … (m,0) 走法也只有1种
除了上面两种情况以外,(i,j) 处的走法等于(i-1,j) + (i,j-1) 的走法之和,即为递推公式
画表格

0	1	1	1	1	1	1
1	2	3	4	5	6	7
1	3	6	10	15	21	28

题解

public class UniquePaths {
    public static void main(String[] args) {
        int count = new UniquePaths().uniquePaths(3, 7);
        System.out.println(count);
    }

 public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n; j++) {
            dp[0][j] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
}

降维

public class UniquePaths {
    public static void main(String[] args) {
        int count = new UniquePaths().uniquePaths(3, 7);
        System.out.println(count);
    }

   public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for (int i = 1; i < m; i++) {
            dp[0] = 1;
            for (int j = 1; j < n; j++) {
                dp[j] = dp[j] + dp[j - 1];
            }
        }
        return dp[n - 1];
}
}

分析

这个代码是解决一个经典的动态规划问题,称为“不同路径问题”(Unique Paths Problem)。问题描述如下:

问题描述

给定一个 m x n 的网格(grid),一个机器人位于左上角 (0, 0),需要移动到右下角 (m-1, n-1)。机器人只能向右或者向下移动。问机器人有多少种不同的路径到达右下角。

解题思路

这个问题可以通过动态规划来求解。动态规划的基本思路是将问题分解为更小的子问题,通过存储子问题的解来避免重复计算。

动态规划的基本步骤
  1. 定义状态

    • 使用一个二维数组 dp 来表示状态,其中 dp[i][j] 表示机器人从左上角 (0, 0) 移动到位置 (i, j) 的不同路径数量。
  2. 状态转移方程

    • 对于位置 (i, j),机器人可以从上方位置 (i-1, j) 或左方位置 (i, j-1) 移动过来。因此,dp[i][j] 的值等于来自上方的路径数量加上来自左方的路径数量:
      [
      dp[i][j] = dp[i-1][j] + dp[i][j-1]
      ]
  3. 初始化

    • 对于最左边的第一列 dp[i][0],机器人只能一直向下移动到达该列,所以所有的 dp[i][0] 初始化为 1
    • 对于最上边的第一行 dp[0][j],机器人只能一直向右移动到达该行,所以所有的 dp[0][j] 初始化为 1
  4. 计算最终结果

    • 根据状态转移方程从上到下、从左到右逐步计算出每个位置的路径数量,最终 dp[m-1][n-1] 就是从 (0, 0)(m-1, n-1) 的不同路径数量。

代码解析

public class UniquePaths {
    public static void main(String[] args) {
        int count = new UniquePaths().uniquePaths(3, 7);
        System.out.println(count);
    }

    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n]; // 创建一个二维数组来保存路径数量

        // 初始化第一列,机器人只能一直向下走,所以每个格子路径数量为1
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        
        // 初始化第一行,机器人只能一直向右走,所以每个格子路径数量为1
        for (int j = 0; j < n; j++) {
            dp[0][j] = 1;
        }
        
        // 填充整个dp表,从第二行和第二列开始,因为第一行和第一列已经初始化
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 路径数量等于上方和左方的路径数量之和
            }
        }
        
        return dp[m - 1][n - 1]; // 返回到达右下角的路径数量
    }
}

代码详解

  1. 初始化二维数组 dp

    • dp 的大小为 m x n,用于保存从起点 (0, 0) 到每个格子 (i, j) 的不同路径数量。
  2. 初始化边界条件

    • dp[i][0] = 1:表示机器人在第一列的任意位置 (i, 0),由于只能向下移动到达这些位置,所以路径数量为 1
    • dp[0][j] = 1:表示机器人在第一行的任意位置 (0, j),由于只能向右移动到达这些位置,所以路径数量为 1
  3. 状态转移计算

    • 从位置 (1, 1) 开始,逐个计算每个位置的路径数量。对于任意位置 (i, j)
      • 机器人可以从上方 (i-1, j) 或左方 (i, j-1) 移动到达 (i, j)
      • 所以 dp[i][j] = 来自上方的路径数量 dp[i-1][j] + 来自左方的路径数量 dp[i][j-1]
  4. 输出结果

    • 最终 dp[m-1][n-1] 保存的是从左上角 (0, 0) 到右下角 (m-1, n-1) 的所有不同路径的总数。

时间复杂度和空间复杂度

  • 时间复杂度O(m * n)。我们需要遍历整个二维数组一次,每个位置计算一次。
  • 空间复杂度O(m * n)。使用了一个 m x n 大小的二维数组来存储中间状态。

示例输出

假设 m = 3n = 7

1  1  1  1  1  1  1
1  2  3  4  5  6  7
1  3  6 10 15 21 28
  • dp[2][6] = 28 表示从 (0, 0)(2, 6)28 条不同路径。

进一步优化(降维)

可以通过空间优化来降低空间复杂度。在计算当前行的路径数量时,只需要上一行的信息。因此,可以只用一维数组来保存当前行的路径数量,从而将空间复杂度从 O(m * n) 降低到 O(n)

1  1  1  1  1  1  1
1  2  3  4  5  6  7
1  3  6 10 15 21 28

观察上述内容,不难发现,对初始行全1,他的后面两行对应自己上方的值(曾经的自己),加上自己左边的值(列数减去一),最左边的始终是1,对应dp【0】,因此,只需要有自己的数据,就可以更新自己在下一行的数据,有在下一行的数据,就可以向右边延伸,扩充整个数组,实际上还是二维,但是通过这种方式,缩减了空间占用

1 1+1 2+1 3+1 4+1 5+1—
1 2+1 3+3 4+6----

public int uniquePaths(int m, int n) {
    int[] dp = new int[n];
    Arrays.fill(dp, 1); // 初始化第一行

    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[j] += dp[j - 1]; // dp[j] = dp[j] + dp[j-1]
        }
    }

    return dp[n - 1];
}

通过这种方式,利用 dp[j] 表示当前位置的路径数量,从而只用一维数组完成整个动态规划计算。

背包问题

0-1 背包问题

public class KnapsackProblem {
    /*
        1. n个物品都是固体,有重量和价值
        2. 现在你要取走不超过 10克 的物品
        3. 每次可以不拿或全拿,问最高价值是多少

            编号 重量(g)  价值(元)                        简称
            1   4       1600           黄金一块   400    A
            2   8       2400           红宝石一粒 300    R
            3   5       30             白银一块         S
            0   1       1_000_000      钻石一粒          D
        1_001_630

        1_002_400
     */

    /*
        1   2   3   4   5   6   7   8   9   10
                    a
                    a               r
                    a               r
        d               da          da  dr  dr
     */

    static class Item {
        int index;
        String name;
        int weight;
        int value;

        public Item(int index, String name, int weight, int value) {
            this.index = index;
            this.name = name;
            this.weight = weight;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Item(" + name + ")";
        }
    }

    public static void main(String[] args) {
        Item[] items = new Item[]{
                new Item(1, "黄金", 4, 1600),
                new Item(2, "宝石", 8, 2400),
                new Item(3, "白银", 5, 30),
                new Item(4, "钻石", 1, 10_000),
        };
        System.out.println(select(items, 10));
    }

    static int select(Item[] items, int total) {
        int[][] dp = new int[items.length][total + 1];
        print(dp);
        Item item0 = items[0];
        for (int j = 0; j < total + 1; j++) {
            if (j >= item0.weight) {
                dp[0][j] = item0.value;
            }
        }
        print(dp);
        for (int i = 1; i < dp.length; i++) {
            Item item = items[i];
            for (int j = 1; j < total + 1; j++) {
                // x: 上一次同容量背包的最大价值
                int x = dp[i - 1][j];
                if (j >= item.weight) {
                    // j-item.weight: 当前背包容量-这次物品重量=剩余背包空间
                    // y: 剩余背包空间能装下的最大价值 + 这次物品价值
                    int y = dp[i - 1][j - item.weight] + item.value;
                    dp[i][j] = Integer.max(x, y);
                } else {
                    dp[i][j] = x;
                }
            }
            print(dp);
        }
        return dp[dp.length - 1][total];
    }

    static void print(int[][] dp) {
        System.out.println("   " + "-".repeat(63));
        Object[] array = IntStream.range(0, dp[0].length + 1).boxed().toArray();
        System.out.printf(("%5d ".repeat(dp[0].length)) + "%n", array);
        for (int[] d : dp) {
            array = Arrays.stream(d).boxed().toArray();
            System.out.printf(("%5d ".repeat(d.length)) + "%n", array);
        }
    }
}

降维

static int select(Item[] items, int total) {
    int[] dp = new int[total + 1];
    for (Item item : items) {
        for (int j = total; j > 0; j--) {
            if (j >= item.weight) { // 装得下
                dp[j] = Integer.max(dp[j], item.value + dp[j - item.weight]);
            }
        }
        System.out.println(Arrays.toString(dp));
    }
    return dp[total];
}

注意:内层循环需要倒序,否则 dp[j - item.weight] 的结果会被提前覆盖

分析

0-1 背包问题概述

0-1 背包问题是一个经典的动态规划问题。在这个问题中,你有一个背包,其最大容量为 total(单位为重量),以及若干个物品,每个物品有一个重量 weight 和一个价值 value。你需要决定如何选择物品,使得放入背包的物品总重量不超过背包的最大容量,同时总价值最大。

在 0-1 背包问题中,每个物品只能选择一次(即每个物品要么放入背包,要么不放入背包),这就是问题名称中的 “0-1” 的含义。

问题描述与代码解析

给定一些物品和背包的最大容量,总目标是找到一种选取物品的方式,使得选取的物品总重量不超过给定的容量,同时其总价值最大。

代码中有两种解法:

  1. 二维数组解法:使用二维动态规划表 dp[i][j] 来保存前 i 个物品在容量 j 的情况下所能得到的最大价值,第一行代表只加入第一个物品,第二行代表两个物品,最后四行,代表总物品种类数,对应了所有物品,列代表的是整体的最大容量限制。在换入新行的时候,容量不够,就依据上一行补数据,容量足够,就进行价值比对,替换更优的数据,做到当行最优,下一行只需要和上一行比对即可。总结对应:装不装得下,装的下,能不能优化价格,做出地推狮在这里插入图片描述2. 一维数组解法(降维优化):使用一维数组 dp[j] 来保存容量为 j 的情况下所能得到的最大价值。

解法 1:二维数组解法

static int select(Item[] items, int total) {
    int[][] dp = new int[items.length][total + 1]; // 创建一个二维数组用于动态规划
    //行数是物品数,列数是0开始,到总数,0-1是负一,所以要特殊处理第一行数据
    
    // 初始化第一个物品的价值
    Item item0 = items[0];
    for (int j = 0; j < total + 1; j++) {
        if (j >= item0.weight) {
            dp[0][j] = item0.value;
        }
    }

    // 遍历所有物品,从第2个物品开始
    for (int i = 1; i < dp.length; i++) {
        Item item = items[i];
        for (int j = 1; j < total + 1; j++) {
            // 取不放当前物品 i 时的最大价值
            int x = dp[i - 1][j];//如果是黄金,i=0,减1就负数了
            if (j >= item.weight) {
                // y 表示当前物品 i 放入背包后的最大价值:放入后的价值加上剩下空间的最大价值
                int y = dp[i - 1][j - item.weight] + item.value;
                dp[i][j] = Integer.max(x, y); // 取两者的最大值
            } else {
                dp[i][j] = x; // 当前物品 i 的重量大于背包容量,不能放入
            }
        }
    }

    return dp[dp.length - 1][total]; // 返回最后一个状态值,即最大价值
}
代码解析
  1. 二维数组 dp 的含义dp[i][j] 表示在前 i 个物品中,容量为 j 的背包所能装入的最大价值。

  2. 初始化第一个物品:对于第一个物品,如果背包容量 j 大于等于第一个物品的重量 item0.weight,那么可以装入背包,dp[0][j] 等于第一个物品的价值 item0.value

  3. 动态规划状态转移

    • x 表示不放当前物品 i 时的最大价值(即前 i-1 个物品的最大价值)。
    • y 表示放当前物品 i 后的最大价值,等于背包容量减去当前物品的重量后的最大价值,加上当前物品的价值。
    • dp[i][j] = Math.max(x, y),表示对于当前容量 j,在前 i 个物品中可以选择的最大价值。
  4. 最终结果dp[dp.length - 1][total] 保存了在所有物品和给定容量 total 下的最大价值。

解法 2:一维数组解法(降维优化)

在这个解法中,我们使用一维数组来优化空间复杂度。

static int select(Item[] items, int total) {
    int[] dp = new int[total + 1]; // 创建一个一维数组用于动态规划

    // 遍历每个物品
    for (Item item : items) {
        // 从后往前遍历背包容量,避免覆盖上一次的计算结果
        for (int j = total; j > 0; j--) {
            if (j >= item.weight) { // 当前容量能装得下当前物品
                dp[j] = Integer.max(dp[j], item.value + dp[j - item.weight]); // 取两者最大值
            }
        }
        System.out.println(Arrays.toString(dp)); // 打印当前状态
    }
    return dp[total]; // 返回最大价值
}
代码解析
  1. 一维数组 dp 的含义dp[j] 表示容量为 j 的背包所能装入的最大价值。

  2. 倒序遍历容量

    • 从后往前遍历 j 是为了防止在更新 dp[j] 时覆盖掉前面已经计算过的值。
    • 每次只考虑一个物品,更新背包从容量 totalitem.weight 的所有可能状态。
  3. 状态转移

    • 如果 j >= item.weight,则当前背包容量可以装下当前物品。
    • dp[j] 更新为两个情况的最大值:
      • 不放当前物品的最大价值:dp[j]
      • 放当前物品的最大价值:item.value + dp[j - item.weight](当前物品的价值加上剩余容量的最大价值)
  4. 空间优化

    • 通过一维数组 dp,将空间复杂度从 O(n * W) 降低到 O(W),其中 W 是背包的容量。

完全背包问题

public class KnapsackProblemComplete {
    static class Item {
        int index;
        String name;
        int weight;
        int value;

        public Item(int index, String name, int weight, int value) {
            this.index = index;
            this.name = name;
            this.weight = weight;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Item(" + name + ")";
        }
    }

    public static void main(String[] args) {
        Item[] items = new Item[]{
                new Item(1, "青铜", 2, 3),    // c
                new Item(2, "白银", 3, 4),    // s
                new Item(3, "黄金", 4, 7),    // a
        };
        System.out.println(select(items, 6));
    }

    /*
            0   1   2   3   4   5   6
        1   0   0   c   c   cc  cc  ccc
        2   0   0   c   s   cc  cs  ccc
        3   0   0   c   s   a   a   ac
     */

    private static int select(Item[] items, int total) {
        int[][] dp = new int[items.length][total + 1];
        Item item0 = items[0];
        for (int j = 0; j < total + 1; j++) {
            if (j >= item0.weight) {
                dp[0][j] = dp[0][j - item0.weight] + item0.value;
            }
        }
        print(dp);
        for (int i = 1; i < items.length; i++) {
            Item item = items[i];            
            for (int j = 1; j < total + 1; j++) {
                // x: 上一次同容量背包的最大价值
            	int x = dp[i - 1][j];
                if (j >= item.weight) {
                    // j-item.weight: 当前背包容量-这次物品重量=剩余背包空间
                    // y: 剩余背包空间能装下的最大价值 + 这次物品价值
                    int y = dp[i][j - item.weight] + item.value;
                    dp[i][j] = Integer.max(x, y);
                } else {
                    dp[i][j] = x;
                }
            }
            print(dp);
        }
        return dp[dp.length - 1][total];
    }

    static void print(int[][] dp) {
        System.out.println("   " + "-".repeat(63));
        Object[] array = IntStream.range(0, dp[0].length + 1).boxed().toArray();
        System.out.printf(("%5d ".repeat(dp[0].length)) + "%n", array);
        for (int[] d : dp) {
            array = Arrays.stream(d).boxed().toArray();
            System.out.printf(("%5d ".repeat(d.length)) + "%n", array);
        }
    }
}

降维

private static int select(Item[] items, int total) {
    int[] dp = new int[total + 1];
    for (Item item : items) {
        for (int j = 0; j < total + 1; j++) {
            if (j >= item.weight) {
                dp[j] = Integer.max(dp[j], dp[j - item.weight] + item.value);
            }
        }
        System.out.println(Arrays.toString(dp));
    }
    return dp[total];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值