📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待
文章目录
- 整数拆分
- 不同的二叉搜索树
- 总结:
本期的两道题都有一些难度,不同的是第一道题看着可能有点想法,但是就是感觉差一点思路,以及代码不好实现,第二道题压根没什么思路,想不到它到底和动态规划的关系在哪,这是我对于初次做这两道题时候的想法。
整数拆分
343. 整数拆分 - 力扣(LeetCode)
整数拆分在leetcode上是一道中等难度的题,它的题目大意是将给定整数n拆解成若干数字乘积的形式,然后求出这些乘积中最大的一项是多少。我们首先要保证拆解出来的数字相加和应当等于原来的整数n。起初在做题的时候,我仅能想到用一个dp数组,且数组内部存取的应当是某个数字的最大乘积,进而一步步推出n的最大乘积,但是剩余的部分想不明白。我们来一起看看题解的解释,根据题解的思路来一步步推出。
dp数组的含义:dp数组含义之前已经说过了,它实际上就是求出第i个数字能够分解出来的数的乘积的最大值。
递推公式:这道题是将一个数字n分解成若干数字然后求最大乘积的题,那么有人就要问了那我们为什么还要用数组保存1-n的全部数字的整数拆分呢?关键点就在这里,我们要将该数分解,就要知道我们是怎么分解的,一个数可以被分解为j*(i-j),j是从1到i的数,例如说分解4,我们可以将其分解为:14,22,31,41,这么四种,也就是刚才的那个公式,j在循环里不断地增大,这样我么就可以使其算式发生变化。那么之前也说过,不一定将n这个整数分解成两个数相乘的形式,只要分解出的数相加等于n那么可以一直分解,例如10这个数我们可以分解为334,这也是合法的,那如果分解成这样我们要如何表示呢?这也是很重要的一点,我们可以想象334是由4乘上9得到的,根据dp数组的记忆我们可以写成是4*dp【9】,这是没毛病的,因为dp数组的定义就是某个数能被拆分后的最大乘积值,所以我们可以根据用dp数组来完成对于多个数字的拆分。
关于j的拆分
我们之前的上面写的公式都是假定j是根据循环变化而变化的值,而不是在递推公式里直接参与拆分的,那我们是否会落下一些情况呢?实际上并不会,每一次的j的变化而引起的数字拆分,是由上向下产生影响的!例如说10可以拆分为22222,那除了第一个2代表j之外,其余的2也可以分成11,但是不要忘记,第一个2也就是j的位置,早在前面j=1时候已经被拆成过11了,所以我们完全不要担心,后面能举出的j的拆分实例,均在j变得更小的时候,完成过类似于拆分的乘积运算了。
下一个我们需要在意的点是,由于递推公式写在j的变化之时,所以dp【i】是一定会随着j而改变的,那如果dp【i】之前已经找到了最大的值,我们还让他不停赋新值,不是会错过了吗,所以我们取一个最大值,即在dp【i】和j*(i-j)和j*dp【i-j}这三个值之中,取出最大值赋给dp【i】以达到保存更新最大值的效用。
dp数组的初始化:初始化就是dp【2】=2,为什么要这么写?因为0和1不能拆分,0就不用说了,1无法拆分成两个整数的相加。
遍历顺序:递推公式就可以得知了,我们推出当前的数字i的最大乘积,完全依赖于之前的数字,所以我们要从前向后遍历。
class Solution {
public:
int integerBreak(int n) {
vector<int>dp(n+1,0);
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=1;j<i;j++)
dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));//dp[i-j]就相当于对i-j的拆分了
}
return dp[n];
}
};
代码还是很简单的,但是想出思路要注意的点,还是蛮多的。
拆分的策略实际上就是固定前面的数字j,不拆分j而通过对于j的增加,而改变后面要拆分的值的策略。
不同的二叉搜索树
96. 不同的二叉搜索树 - 力扣(LeetCode)
这道题是很难的,可以说是困难题了。首先一定要审好题,这是一个求不同的二叉搜索树的题,是二叉搜索树而不是别的什么,强调这一点,是因为二叉搜索树是有一定的规律的。
这道题为什么说它难呢?递推公式比上一道题还难想,我们是根据分别列出n=1,n=2和n=3三种情况的二叉搜索树的形状,来找出一定的规律。n=1只有一种情况就是一个节点,n=2是一个八字形,即1开头的话那么就是有右子树2,2开头的话就是有一个左子树1,没有其他情况!因为这是搜索树,要严格按照搜索树的特点。n=3就分为1开头2开头和3开头三种情况,其中画图可知1和3开头的情况是和n=2的情况是一样的,两个八字形,而n以2开头是和n=1情况一样,是左右子树都平均的情况。这样算下来确实和题目案例对上了,n=3时候有五种情况,而这里我们是考虑二叉树的形状,而非二叉树节点值的不同,所以我们将n=3的以各个数字开头的情况和n=1和n=2的情况做了类比。
dp数组的含义:这里我们明确dp数组的含义实际上是用来保存数值i有多少种不同的二叉树。
递推公式
我们在上面已经分析了很多了,这里再来好好的看看递推公式的规律。 n=3的情况就是元素以1开头情况加上以2开头加上以3开头的情况。
而以1开头数量实际上等于右子树的有两个元素的情况*左子树有0个元素都情况
以2开头的数量等于右子树有一个元素的情况*左子树有1个元素的情况
以3开头的数量等于右子树有0个元素的情况*左子树有两个元素的情况
那么为什么是以乘法的形式得出的呢?而不是加法?我们假设左子树有五种元素情况,右子树有十种,那是不是左子树的每一种和右子树的十种都可以构造出不同的情况呢?所以我们这里用的公式就一定是乘法。
而有两个元素的情况就是n=2,也就是dp【2】,有1个就是dp【1】,0个就是dp【0】(这里看不懂,需要回想dp数组的含义是什么)
知道了这样的一个规律后,我们就可以推出dp【i】+=dp【j-1】*dp【i-j】
j-1是求左子树,i-j是求右子树,这里如果想不明白可以用n=3的各种情况带入,就能证明该公式一定是对的,那么为什么是+=而不是等于呢?这是因为我们要将各个情况都加进去,也就是1-n的所有开头情况加在一起,才是为n时的总二叉树个数。
dp初始化:dp的初始化就仅仅将dp【0】=1就可以了。0个节点也属于一种二叉树的情况。根据0
进而推出1,2,3等等。
遍历顺序:很明显也是一道从前往后遍历,因为要知道前面的情况才能得出n的情况。
class Solution {
public:
int numTrees(int n) {
vector<int>dp(n+1);
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];
}
return dp[n];
}
};
这就是本题的代码了。虽然本题代码很简短,但是代码很难想出。
首先能想到用动态规划做就有点难度,其次是规律十分难找,我们要能想出用前面的情况推出此时以1-n开头的各顶点情况能与之前的1到n-1的各种情况相呼应是很难的,再其次我们用以表示左右二叉树的j-1和i-j这也是很难想出来的,可能需要一定的数学推理。这些条件都使这道题变得毫无头绪,不知道入手点在哪里,自然就做不出来。
总结:
今天我们完成了整数拆分&&不同的二叉搜索树两道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~