343. 整数拆分 - 力扣(LeetCode)
数学方法
观察数字拆分的模式,我们可以发现以下事实:
- 将数字拆分为尽可能多的3会使乘积最大化。这是因为当 n > 4 时,3(n-3) > n,所以我们总是更喜欢3的拆分,而不是保持n。
- 当剩下的数字为4时,拆分为2 * 2是更好的选择。
根据上面的事实,我们可以写出以下算法:
- 如果 n == 2,返回1(因为2只能拆分为1 + 1)。
- 如果 n == 3,返回2(因为3只能拆分为2 + 1)。
- 如果 n == 4,返回4(因为4可以拆分为2 + 2)。
- 对于所有其他的 n,我们可以采用如下方式计算结果:
- 用 n 除以3,得到商 a 和余数 b。
- 如果 b == 0,则结果为 3^a。
- 如果 b == 1,则结果为 3^(a-1) * 4(我们从三的倍数中拿出一个3与余数1组成4)。
- 如果 b == 2,则结果为 3^a * 2。
以下是基于上述方法的Python实现:
def integerBreak(n: int) -> int:if n == 2:return 1if n == 3:return 2if n == 4:return 4# n > 4a, b = divmod(n, 3)if b == 0:return 3**aelif b == 1:return 3**(a-1) * 4else: # b == 2return 3**a * 2# 测试
print(integerBreak(2)) # 输出:1
print(integerBreak(10)) # 输出:36
这种方法的时间复杂度是 O(1),因为我们只是做了一些数学计算。
动态规划
对于整数拆分问题,动态规划的一个直观思路是使用一个数组 dp,其中 dp[i] 表示数字 i 拆分后的最大乘积。
我们的目标是填充这个数组,以便 dp[n] 给出答案。
递推关系的建立需要考虑如何拆分整数 i。对于任何小于 i 的正整数 j,我们可以将 i 拆分为 j 和 i-j。这两个数字可以进一步拆分或保持原样。因此,乘积的最大值是 max(j, dp[j]) * max(i-j, dp[i-j])。
要计算 dp[i],我们考虑所有可能的 j(从1到i/2),并采取上述乘积的最大值。
基于上述逻辑,我们可以为问题建立动态规划解决方案。
def integerBreak(n: int) -> int:# dp[i] 表示数字 i 拆分后的最大乘积dp = [0] * (n+1)dp[1] = 1for i in range(2, n+1):for j in range(1, i//2 + 1):dp[i] = max(dp[i], max(j, dp[j]) * max(i-j, dp[i-j]))return dp[n]# 测试
print(integerBreak(2)) # 输出:1
print(integerBreak(10)) # 输出:36
这种动态规划方法的时间复杂度是 O(n^2),因为我们使用了两层循环来填充 dp 数组。
动态规划优化
优化动态规划的方法涉及减少不必要的计算。对于整数拆分问题,我们注意到当我们选择拆分整数 i
为 j
和 i-j
时,由于问题是对称的,我们实际上只需要考虑从 1
到 i//2
的整数 j
。因此,我们可以减少一半的计算。
考虑到这一点,我们仍然需要确定如何拆分整数以获得最大的乘积。动态规划的核心仍然是使用 dp
数组,其中 dp[i]
表示整数 i
被拆分后的最大乘积。
考虑到我们的数学方法,整数3和2是最关键的因素。我们可以进一步优化我们的动态规划解决方案,使其更接近数学解决方案,但仍然使用动态规划的框架。
这是优化后的解决方案:
def integerBreak(n: int) -> int:if n == 2: return 1if n == 3:return 2# dp[i] 表示数字 i 拆分后的最大乘积dp = [0] * (n+1)# 基本情况dp[1], dp[2], dp[3] = 1, 2, 3for i in range(4, n+1):# 因为问题对称,我们只考虑从1到i//2的整数jfor j in range(1, i//2 + 1):dp[i] = max(dp[i], dp[j] * dp[i-j])return dp[n]# 测试
print(integerBreak(2)) # 输出:1
print(integerBreak(10)) # 输出:36
尽管外观上与先前的动态规划解决方案相似,但这个解决方案的性能更好,因为它减少了不必要的计算,并且更加接近数学解决方案的思路。
96. 不同的二叉搜索树 - 力扣(LeetCode)
这个问题可以使用动态规划来解决,基于以下观察:
当我们尝试构建一个二叉搜索树时,我们可以选择一个数字作为根。如果我们选择数字 i 作为根,那么所有小于 i 的数字必须位于它的左子树中,而所有大于 i 的数字则必须位于它的右子树中。
因此,对于一个给定的根 i,数量是:左子树的数量乘以右子树的数量。
我们可以使用动态规划来计算所有可能数量的总和。定义数组 G
,其中 G[n]
表示长度为 n
的序列的不同二叉搜索树的数量。
给定序列 1 … n,我们从序列中选择一个数字 i,将该数作为根,将 1 … (i-1) 序列作为左子树,将 (i+1) … n 序列作为右子树。因此,我们可以得出以下公式:
[ G(n) = G(0) \times G(n-1) + G(1) \times G(n-2) + … + G(n-1) \times G(0) ]
具体解决方案如下:
def numTrees(n: int) -> int:G = [0] * (n + 1)G[0], G[1] = 1, 1for i in range(2, n + 1):for j in range(1, i + 1):G[i] += G[j - 1] * G[i - j]return G[n]# 测试
print(numTrees(3)) # 输出:5
print(numTrees(1)) # 输出:1
这个方法的时间复杂度是 ( O(n^2) )。