页面启动中 . . .

动态规划思路和例题应用


动态规划思路和例题应用

dp实在是用得太多了,这里的例题都不知道怎么举例了。本文主要讲讲思路和步骤,典型的分类有01背包问题,也归纳出一个适合自己的模板。(包括之前写过一点的股票问题,也是dp的典中典系列,后续会接着更新~)

一、动态规划思路步骤

1. 动态规划的定义和区别

  • 动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

  • 所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

    例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

    • 动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

    • 但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

2. 动态规划的解题步骤

在dp的典型题目中,状态的转移公式(递推公式)是最重要的,但是并不是唯一需要注意的。一个良好的解题,需要完整的方法。下面总结一种比较好的解题步骤。

  • 五步曲:

    1. 确定dp数组以及下标的含义
    2. 确定递推公式
    3. dp数组的初始化
    4. 确定遍历的顺序
    5. 举例推导出dp数组验证
  • 注意事项

    1. 写dp相关的问题,会在推导的时候出现模糊的情况,这里一定要将dp数组,不如说是dp[i-1][j-1]代表的含义自己思考一次。

    2. 写代码之前,将状态转移在dp数组上的具体情况模拟一次,如果存在问题,说明是上述的5步中存在问题,常见的有遍历顺序、初始化、递归公式三部分。

    3. 遇到问题,其实可以自己先思考这三个方面:

      • 这道题目我举例推导状态转移公式了么?
      • 我打印dp数组的日志了么?
      • 打印出来了dp数组和我想的一样么?

二、动态规划典型例题

1. 整数拆分

(1)题目说明
  • 给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

  • 返回 你可以获得的最大乘积

    示例 1:

    输入: n = 2
    输出: 1
    解释: 2 = 1 + 1, 1 × 1 = 1。

    示例 2:

    输入: n = 10
    输出: 36
    解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

(2)解答分析

拆分为k个整数,那究竟是多少个呢?此时可以利用动态规划dp进行分析。下面根据五部曲来试试。

  1. 确定dp数组的含义和下标的定义
  • dp[i]: 题目最后要求什么?是拆分数字i后得到的k个数的乘积的最大值!

  • 所以此时的数组可以设为:拆分数字i,可以得到的最大乘积为dp[i]

  1. 确定递推公式
  • dp[i]最大乘积是怎么得到的呢?其实不难发现,将i进行拆分,实际是从1开始遍历到j,存在两种渠道得到dp[i]

    • 一个是直接j*(i-j)得到乘积;
    • 一个是将j*dp[i-j],相当于是拆分了(i-j),可以不仅仅是两个数,还有可能是之前状态下的多种可能;
  • j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j 取最大的。

  • 递推公式为:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

  1. dp的初始化
  • 对严格的dp初始化而言,需要根据刚才的递推公式进行讨论。这里只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,这就是简单的初始化。

  1. 确定遍历顺序
  • 根据已经推导出来的公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));

    • dp[i]是依靠dp[i-1]的状态,所以遍历i一定是从前向后遍历,先是存在dp[i-j]再有dp[i]
  • 所以有这样的遍历顺序:

    for (int i = 3; i <= n ; i++) { //初始化从2开始
        for (int j = 1; j < i - 1; j++) {
            dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
        }
    }
  • 注意 枚举j的时候,是从1开始的。从0开始的话,那么让拆分一个数拆个0,求最大乘积就没有意义了。

    • j的结束条件是 j < i - 1 ,其实 j < i 也是可以的,不过可以节省一步,例如让j = i - 1,的话,其实在 j = 1的时候,这一步就已经拆出来了,重复计算,所以 j < i - 1

    • 至于 i是从3开始,这样dp[i - j]就是dp[2],正好可以通过我们初始化的数值求出来。

  1. 举例推导出dp数组
  • 当n为10的时候,dp数组里的数值,如下:

    举例

(3)具体代码
class Solution {
    public int integerBreak(int n) {
        //dp[i] 为正整数 i 拆分后的结果的最大乘积
        int[] dp = new int[n+1];
        dp[2] = 1;
        for(int i = 3; i <= n; i++) {
            for(int j = 1; j <= i-j; j++) {
                // 这里的 j 其实最大值为 i-j,再大只不过是重复而已,
                //并且,在本题中,我们分析 dp[0], dp[1]都是无意义的,
                //j 最大到 i-j,就不会用到 dp[0]与dp[1]
                dp[i] = Math.max(dp[i], Math.max(j*(i-j), j*dp[i-j]));
                // j * (i - j) 是单纯的把整数 i 拆分为两个数 也就是 i,i-j ,再相乘
                //而j * dp[i - j]是将 i 拆分成两个以及两个以上的个数,再相乘。
            }
        }
        return dp[n];
    }
}
  • 复杂度分析:

    • 时间复杂度: O(n2)O(n^2)

      在上述数组的遍历中,当对i进行取值的同时,也维护了j作为最大的乘积值的特征

    • 空间复杂度: O(n)O(n)

      通过引入了dp数组,对每个正整数i进行结果的拆分,最坏的情况下可以得到n+1组不同的取值结果,因此此时的空间复杂度为O(n)

2. 背包问题

早有耳闻背包问题,这里有关背包和物品之间的关系,可以采用动态规划进行修改判断。

这里简单介绍的是01类型的背包问题
  • 介绍:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

  • 举个例子:

    重量 价值
    物品0 1 15
    物品1 3 20
    物品2 4 30
  • 求解背包能背的物品的最大价值是多少?

利用二维dp数组求解
  1. 确定dp数组的下标和含义

    • 使用二维数组,dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

  2. 确定递推公式

    那么可以有两个方向推出来dp[i][j]

    • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
    • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

    所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  3. dp数组如何初始化

    • 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

    • dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

    • j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

    • j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

  4. 确定遍历顺序

    • 给出先遍历物品,然后遍历背包重量的代码,这个好理解一些。
    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    
        }
    }

  5. 举例推导dp数组

    举例2

具体代码实现
  • 这里给出在IDEA中编写的完整题目代码,其中存在两个类似的函数,分别是二维dp数组和一维dp数组的方法:
import java.util.Scanner;

public class dpBeibao {
    public static void main(String[] args) {
        Scanner in=new Scanner(System.in);
        int wn=in.nextInt(); //物品的个数,用其重量做分类
        int vn=in.nextInt(); //表示物品价值的个数
        int bagSize=in.nextInt(); //表示包的最大容量
        int[] weight = new int[wn];
        for(int i=0;i<wn;i++){
            weight[i]=in.nextInt();
        }
        int[] value = new int[vn];
        for(int i=0;i<vn;i++){
            value[i]=in.nextInt();
        }
//        int[] weight={1,3,4};
//        int[] value = {15,20,30};

        testWeightBagProblem(weight,value,bagSize);
        System.out.println("------------------");
        testWeightBagProblem_1(weight,value,bagSize);

    }

    /**
     * 动态规划获得结果
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    /*
                      当前背包的容量都没有当前物品i大的时候,是不放物品i的
                      那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i-1][j];
                } else {
                    /*
                      当前背包的容量可以放下物品i
                      那么此时分两种情况:
                         1、不放物品i
                         2、放物品i
                      比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }

        // 打印dp数组
        for (int i = 0; i < goods; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }

    }
    //或者尝试一维数组,直接保存当前的重量和价值的数组即可
    public static void testWeightBagProblem_1(int[] weight, int[] value, int bagWeight) {
        int wLen = weight.length;
        //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
        int[] dp = new int[bagWeight + 1];
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 0; i < wLen; i++) {
            for (int j = bagWeight; j >= weight[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        //打印dp数组
        for (int j = 0; j <= bagWeight; j++) {
            System.out.print(dp[j] + " ");
        }
        System.out.println();
    }

    /*输入:
    3 3 4
    1 3 4
    15 20 30
    */
}
  • 最后输出:

    输出结果
  • 我们只需要选取的是最后一个就行,也就是选择返回:System.out.println(dp[goods-1][bagSize]);即可。


文章作者: XKJ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XKJ !
  目录