本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
本篇文章是 代码随想录算法训练营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[i-1][j] 是求出不放入
- 放入物品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 <= 2001 <= 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 <= 5001 <= mines.length <= 50000 <= 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数组里面的内容, 代码忘记删了
总结
又一天的算法实战结束了, 奥利给
本文内容到此结束了
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位大佬指出。
我是 宁轩 , 我们下次再见