前言
本篇文章是 代码随想录算法训练营day41, 42, 43 的部分内容, 因为十月的拖更, 要补回来, 训练营应该是19号结束, 还有十天结束, 我却还有整整四十天的内容, 死亡...
今日任务:
- 背包问题理论基础
- 96. 不同的二叉搜索树
- 416. 分割等和子集
- 764. 最大加号标志(22.11.9每日一题)
动态规划解题五部曲
- 确定 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` 来演示 复制代码
- 确定
dp数组
及其下标的含义
在dp[i][j]
中, 我们使用i
来代表物品j
来代表重量, 在java
中数组默认值为0
, 我这边示例图也就以0
表示初始创建好的dp数组
,具体如下图所示
- 确定递推公式有两种情况, 一种情况是算上当前物品就超重了, 一种是可以添加上当前物品
- 放入物品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
的最大价值
dp数组
的初始化 初始化时, 为了方便我们后续的操作, 就将所有元素都转为0
,同时, 由于计算的时候, 涉及到多个i-1
操作, 所以我们的dp数组
初始大小为 dp[weight.length + 1][bagsize + 1]
- 确认遍历顺序
我采用先i
后j
遍历, 也就是先按照 重量 进行遍历, 在按照 物品 进行遍历
// 遍历 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]); } } } 复制代码
- 举例推导
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
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入: n = 3 输出: 5 复制代码
示例 2:
输入: n = 1 输出: 1 复制代码
提示:
1 <= n <= 19
思路分析
- 确定
dp数组
及其下标的含义
本次采用一维数组
数组下标的含义为: 第n
个数有几种不同的二叉搜索树 - 确定递推公式
这道题的推导公式很不好理解, 如果觉得我讲的不明白的可以参考一下 代码随想录 - 不同的二叉搜索树一文
下面列出 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];
dp数组
的初始化 从定义上来讲, 空节点也是一颗二叉树
从递推公式来讲, 也要求 dp[0] == 1, 不然乘法就会出现0
了, 这也不是我们想要的
所以 dp[0] = 1- 确认遍历顺序
从前往后遍历, 因为后面的要依赖于前面节点数结果
for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { dp[i] += dp[j - 1] * dp[i - j]; } } 复制代码
- 举例推导
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
的子集 - 背包中的元素不可重复放入
- 确定
dp数组
及其下标的含义
一维数组, dp[j] : 容量为j
最大的子集总和为 dp[j] - 确定递推公式
和我们理论基础那道题一样, 相当于往背包里面放入元素, 那么物品i
的重量为nums[i]
, 价值也是nums[i]
dp[i] = Math.max(dp[j], dp[j - nums[i]] + nums[i]) dp数组
的初始化 本题直接dp[0] = 0
即可- 确认遍历顺序
如果使用一维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]); } } 复制代码
- 举例推导
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
,其他每个元素都为 1
。mines[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)
都 不重复
思路分析
- 确定 dp 数组以及下标的含义
dp[i][j] 表示以当前坐标为中心的最大加号阶数 - 确定递推公式
因为是求加号的阶数, 那么只要求出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); 复制代码
- dp 数组如何初始化
赋予 dp数组 上的所有值为 n, mines 坐标值为 0 - 确认遍历顺序
分为上下左右四个方向进行遍历
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); } } 复制代码
- 举例推导 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数组里面的内容, 代码忘记删了
总结
又一天的算法实战结束了, 奥利给