动态规划第三弹 难度提升 从背包问题理论基础讲起

简介: 动态规划第三弹 难度提升 从背包问题理论基础讲起

前言

本篇文章是 代码随想录算法训练营day41, 42, 43 的部分内容, 因为十月的拖更, 要补回来, 训练营应该是19号结束, 还有十天结束, 我却还有整整四十天的内容, 死亡...

今日任务:

动态规划解题五部曲

  • 确定 dp 数组以及下标的含义
  • 确定递推公式
  • dp 数组如何初始化
  • 确认遍历顺序
  • 举例推导 dp 数组

动态规划之背包问题理论基础

关于背包问题, 共分为以下几种:

  • 01背包
  • 完全背包
  • 多重背包
  • 分组背包

对于我们大多数人来讲, 学会熟悉 01背包完全背包 就可以了

网络异常,图片无法展示
|

(图片来源于代码随想录)

下面我们就根据背包问题模拟一道题目

题目描述

n 件物品和能装 bagsize 重量的背包, weight[i] 代表第 i 件物品的重量, value[i] 代表第 i 件物品的价值

问: 将哪些物品装入背包的价值总和最高

注: 每件物品只能被装入一次

示例1:

网络异常,图片无法展示
|

输入: int[] weight = {1, 3, 4}       int[] value = {15, 20, 30}        int bagsize = 4;
输出: 35
复制代码

思路分析

在思路分析中, 我们采用 `示例1` 来演示 
复制代码
  1. 确定 dp数组 及其下标的含义
    dp[i][j] 中, 我们使用 i 来代表物品 j 来代表重量, 在 java 中数组默认值为 0, 我这边示例图也就以 0 表示初始创建好的 dp数组 ,具体如下图所示

网络异常,图片无法展示
|

  1. 确定递推公式有两种情况, 一种情况是算上当前物品就超重了, 一种是可以添加上当前物品
  • 放入物品i 如果放入物品i, 那么 dp[i][j] 的重量应该是: dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i - 1]]+value[i])
  • dp[i-1][j] 是求出不放入 物品i 的价值和
  • dp[i-1][j-weight[i - 1]]+value[i] 是求出不放入 物品i 的最大价值, 再加上 物品i 的最大价值
  1. dp数组 的初始化 初始化时, 为了方便我们后续的操作, 就将所有元素都转为 0,同时, 由于计算的时候, 涉及到多个 i-1 操作, 所以我们的 dp数组 初始大小为 dp[weight.length + 1][bagsize + 1]

网络异常,图片无法展示
|

  1. 确认遍历顺序
    我采用先 ij 遍历, 也就是先按照 重量 进行遍历, 在按照 物品 进行遍历
// 遍历 dp数组
for (int i = 1; i < dp.length; i++) {
    for (int j = 1; j <= bagsize; j++) {
        if (j < weight[i - 1]){
            dp[i][j] = dp[i-1][j];
        }else{
            dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i - 1]]+value[i - 1]);
        }
    }
}
复制代码
  1. 举例推导 dp数组推导就按照我们的 举例1 进行推导, 推导结果如下图

网络异常,图片无法展示
|

代码展示

public static int pack01(int[] weight, int[] value, int bagsize){
    // 创建 dp数组, 初始化 dp数组
    final int[][] dp = new int[weight.length + 1][bagsize+1];
    // 遍历 dp数组
    for (int i = 1; i < dp.length; i++) {
        for (int j = 1; j <= bagsize; j++) {
            if (j < weight[i - 1]){
                dp[i][j] = dp[i-1][j];
            }else{
                dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i - 1]]+value[i - 1]);
            }
        }
    }
    return dp[weight.length][bagsize];
}
复制代码

96. 不同的二叉搜索树

题目描述

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

网络异常,图片无法展示
|

输入: n = 3
输出: 5
复制代码

示例 2:

输入: n = 1
输出: 1
复制代码

提示:

  • 1 <= n <= 19

思路分析

  1. 确定 dp数组 及其下标的含义
    本次采用 一维数组 数组下标的含义为: 第 n 个数有几种不同的二叉搜索树
  2. 确定递推公式
    这道题的推导公式很不好理解, 如果觉得我讲的不明白的可以参考一下 代码随想录 - 不同的二叉搜索树一文
    下面列出 n = 1 ~ 3 时, 二叉搜索树的状态, 图片来源于代码随想录

网络异常,图片无法展示
|

网络异常,图片无法展示
|

n = 3 时 有三种情况:

  • 1 为头结点时, 右树的两个节点布局, 如果 不考虑数值 的情况下, 他是和 n = 2 时两棵树的布局一样
  • 2 为头结点时, 布局和 n = 1 一样
  • 3 为头结点时, 布局和 n = 2 一样

所以, n = 3 的布局数量, 就等于以 1, 2, 3 为头结点的二叉搜索树种类之和

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。

有1个元素的搜索树数量就是dp[1]。

有0个元素的搜索树数量就是dp[0]。

所以 dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

由此可推出: dp[i] += dp[j - 1] * dp[i - j];

  1. dp数组 的初始化 从定义上来讲, 空节点也是一颗二叉树
    从递推公式来讲, 也要求 dp[0] == 1, 不然乘法就会出现 0 了, 这也不是我们想要的
    所以 dp[0] = 1
  2. 确认遍历顺序
    从前往后遍历, 因为后面的要依赖于前面节点数结果
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
        dp[i] += dp[j - 1] * dp[i - j];
    }
}
复制代码
  1. 举例推导 dp数组

n = 5 时, 推导结果如下所示

网络异常,图片无法展示
|

代码展示

public int numTrees(int n) {
    // 创建 dp数组
    int[] dp = new int[n+1];
    // 初始化 dp数组
    dp[0] = 1;
    dp[1] = 1;
    // 遍历从 i = 2 开始
    for (int i = 2; i <= n; i++) {
        // 当 j = 1 时, 二叉树左边为空, 当 j = i 时, 二叉树右边为空
        for (int j = 1; j <= i; j++) {
            dp[i] += dp[j-1] * dp[i-j];
        }
    }
    return dp[n];
}
复制代码

提交结果

网络异常,图片无法展示
|

416. 分割等和子集(01背包问题)

题目描述

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入: nums = [1,5,11,5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11] 。
复制代码

示例 2:

输入: nums = [1,2,3,5]
输出: false
解释: 数组不能分割成两个元素和相等的子集。
复制代码

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

思路分析

本题依旧采用 01背包 解法, 因为本题所有元素只能被使用一次, 只有确定以下四点, 才能把 01背包问题套到本题上来

  • 背包的体积为 sum/2
  • 背包放入的商品重量为 元素的数值, 价值也为 元素的数值
  • 如果背包正好装满, 说明找到了总和为 sum/2 的子集
  • 背包中的元素不可重复放入
  1. 确定 dp数组 及其下标的含义
    一维数组, dp[j] : 容量为 j 最大的子集总和为 dp[j]
  2. 确定递推公式
    和我们理论基础那道题一样, 相当于往背包里面放入元素, 那么 物品i 的重量为 nums[i], 价值也是 nums[i]dp[i] = Math.max(dp[j], dp[j - nums[i]] + nums[i])
  3. dp数组 的初始化 本题直接 dp[0] = 0 即可
  4. 确认遍历顺序
    如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历
for(int i = 0; i < n; i++){
        for(int j = target; j >= nums[i]; j--){
            //物品 i 的重量是 nums[i],其价值也是 nums[i]
            dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
        }
    }
复制代码
  1. 举例推导 dp数组
    如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。

网络异常,图片无法展示
|

代码展示

// 如果数组为空 则直接返回 false
    if (nums == null || nums.length == 0){
        return false;
    }
    // 总和初始化
    int sum = 0;
    // 计算 nums 数组总和
    for (int num : nums) {
        sum += num;
    }
    // 判断总和是否能平分, 如果不能平分则代表肯定不能分为两个 和相等 的子集
    if (sum % 2 != 0){
        return false;
    }
    // 计算平均数
    int target = sum/2;
    int[] dp = new int[target + 1];
    // 如果 dp数组 是一维数组, 则物品放外面, 且内循环倒序
    for (int i = 0; i < nums.length; i++) {
        for (int j = target; j >= nums[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        }
    }
    return dp[target] == target;
复制代码

提交结果

网络异常,图片无法展示
|

764. 最大加号标志(22.11.9每日一题)

题目描述

在一个 n x n 的矩阵 grid 中,除了在数组 mines 中给出的元素为 0,其他每个元素都为 1mines[i] = [xi, yi]表示 grid[xi][yi] == 0

返回  **grid中包含 1 的最大的 轴对齐 加号标志的阶数 。如果未找到加号标志,则返回 0

一个 k 阶由 1 组成的  “轴对称”加号标志 具有中心网格 grid[r][c] == 1 ,以及4个从中心向上、向下、向左、向右延伸,长度为 k-1,由 1 组成的臂。注意,只有加号标志的所有网格要求为 1 ,别的网格可能为 0 也可能为 1

示例 1:

网络异常,图片无法展示
|

输入: n = 5, mines = [[4, 2]]
输出: 2
解释: 在上面的网格中,最大加号标志的阶只能是2。一个标志已在图中标出。
复制代码

示例 2:

网络异常,图片无法展示
|

输入: n = 1, mines = [[0, 0]]
输出: 0
解释: 没有加号标志,返回 0 。
复制代码

提示:

  • 1 <= n <= 500
  • 1 <= mines.length <= 5000
  • 0 <= xi, yi < n
  • 每一对 (xi, yi)不重复

思路分析

  1. 确定 dp 数组以及下标的含义
    dp[i][j] 表示以当前坐标为中心的最大加号阶数
  2. 确定递推公式
    因为是求加号的阶数, 那么只要求出 dp[i][j] 点四个方向上最小的 连续1个数 就行了 将以 i 为原点, 进行遍历判断最小阶级求取四个方向上的最小阶级, j 是从 0 => n-1, k 是从 n-1 => 0
left = dp[i][j] > 0 ? left + 1 : 0;
        right = dp[i][k] > 0 ? right + 1 : 0;
        up = dp[j][i] > 0 ? up + 1 : 0;
        down = dp[k][i] > 0 ? down + 1 : 0;
        dp[i][j] = Math.min(dp[i][j], left);
        dp[i][k] = Math.min(dp[i][k], right);
        dp[j][i] = Math.min(dp[j][i], up);
        dp[k][i] = Math.min(dp[k][i], down);
复制代码
  1. dp 数组如何初始化
    赋予 dp数组 上的所有值为 n, mines 坐标值为 0
  2. 确认遍历顺序
    分为上下左右四个方向进行遍历
for (int i = 0; i < n; ++i) {
       int left = 0, right = 0, up = 0, down = 0;
       for (int j = 0, k = n - 1; j < n; ++j, --k) {
           left = dp[i][j] > 0 ? left + 1 : 0;
           right = dp[i][k] > 0 ? right + 1 : 0;
           up = dp[j][i] > 0 ? up + 1 : 0;
           down = dp[k][i] > 0 ? down + 1 : 0;
           dp[i][j] = Math.min(dp[i][j], left);
           dp[i][k] = Math.min(dp[i][k], right);
           dp[j][i] = Math.min(dp[j][i], up);
           dp[k][i] = Math.min(dp[k][i], down);
       }
   }
复制代码
  1. 举例推导 dp 数组
    当 n = 5, mines = [[4, 2]] 时, 推导结果如下所示

网络异常,图片无法展示
|

代码展示

public static int orderOfLargestPlusSign(int n, int[][] mines) {
        // 初始化 dp数组
        int[][] dp = new int[n][n];
        // 为 dp数组 上的所有元素 赋予 n
        for (var e : dp) {
            Arrays.fill(e, n);
        }
        // 将 mines 上的坐标添加到 dp数组中
        for (var e : mines) {
            dp[e[0]][e[1]] = 0;
        }
        // 遍历
        for (int i = 0; i < n; ++i) {
            // 以坐标 [i, i] 为中心, 遍历计算上下左右坐标的最小阶级
            int left = 0, right = 0, up = 0, down = 0;
            for (int j = 0, k = n - 1; j < n; ++j, --k) {
                left = dp[i][j] > 0 ? left + 1 : 0;
                right = dp[i][k] > 0 ? right + 1 : 0;
                up = dp[j][i] > 0 ? up + 1 : 0;
                down = dp[k][i] > 0 ? down + 1 : 0;
                dp[i][j] = Math.min(dp[i][j], left);
                dp[i][k] = Math.min(dp[i][k], right);
                dp[j][i] = Math.min(dp[j][i], up);
                dp[k][i] = Math.min(dp[k][i], down);
            }
        }
        // 最大阶级记录
        int ans = 0;
        // 遍历获得最大阶级
        for (var e : dp) {
            ans = Math.max(ans, Arrays.stream(e).max().getAsInt());
        }
        return ans;
    }
复制代码

提交结果

超时是因为之前在 idea 里面敲代码的时候, 有打印 dp数组里面的内容, 代码忘记删了

网络异常,图片无法展示
|

总结

又一天的算法实战结束了, 奥利给



目录
相关文章
|
3月前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
|
5月前
|
算法 Java 决策智能
Java数据结构与算法:动态规划之背包问题
Java数据结构与算法:动态规划之背包问题
|
6月前
|
存储 缓存 算法
【算法训练-动态规划 零】动态规划解题框架
【算法训练-动态规划 零】动态规划解题框架
96 0
|
6月前
|
算法 数据安全/隐私保护 决策智能
【算法训练-回溯算法 零】回溯算法解题框架
【算法训练-回溯算法 零】回溯算法解题框架
61 0
|
存储 人工智能 分布式计算
动态规划从理论到实践-深入理解贪心/分治/回溯/动态规划的算法思想
动态规划从理论到实践-深入理解贪心/分治/回溯/动态规划的算法思想
233 0
|
存储 人工智能 算法
【算法分析与设计】动态规划(上)
【算法分析与设计】动态规划(上)
|
算法 C语言
【软考总结】-<算法>动态规划法--0-1背包问题
在上篇博客中,我们讲了动态规划法在最长公共子序列问题中的应用,(详情请见博客《算法:动态规划法--最长公共子序列》)。这次学习了0-1背包问题,对动态规划法的思想理解了更深了一层,以下是我的简单理解,希望能为路过的读者带来帮助。
261 0
|
算法 Java
数据结构与算法之打家劫舍(二)&&动态规划思想
数据结构与算法之打家劫舍(二)&&动态规划思想
105 0
数据结构与算法之打家劫舍(二)&&动态规划思想
|
算法 Java C++
数据结构与算法之打家劫舍(一)&&动态规划思想
数据结构与算法之打家劫舍(一)&&动态规划思想
112 0
数据结构与算法之打家劫舍(一)&&动态规划思想
|
算法
算法设计与分析/数据结构与算法实验7:0-1背包问题(分支限界法)
算法设计与分析/数据结构与算法实验7:0-1背包问题(分支限界法)
233 0
算法设计与分析/数据结构与算法实验7:0-1背包问题(分支限界法)