秋招算法备战第41天 | 343. 整数拆分、96.不同的二叉搜索树

343. 整数拆分 - 力扣(LeetCode)

数学方法

观察数字拆分的模式,我们可以发现以下事实:

  1. 将数字拆分为尽可能多的3会使乘积最大化。这是因为当 n > 4 时,3(n-3) > n,所以我们总是更喜欢3的拆分,而不是保持n。
  2. 当剩下的数字为4时,拆分为2 * 2是更好的选择。

根据上面的事实,我们可以写出以下算法:

  1. 如果 n == 2,返回1(因为2只能拆分为1 + 1)。
  2. 如果 n == 3,返回2(因为3只能拆分为2 + 1)。
  3. 如果 n == 4,返回4(因为4可以拆分为2 + 2)。
  4. 对于所有其他的 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 1
    if n == 3:
        return 2
    if n == 4:
        return 4
    
    # n > 4
    a, b = divmod(n, 3)
    
    if b == 0:
        return 3**a
    elif b == 1:
        return 3**(a-1) * 4
    else:  # b == 2
        return 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] = 1

    for 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 数组。

动态规划优化

优化动态规划的方法涉及减少不必要的计算。对于整数拆分问题,我们注意到当我们选择拆分整数 iji-j 时,由于问题是对称的,我们实际上只需要考虑从 1i//2 的整数 j。因此,我们可以减少一半的计算。

考虑到这一点,我们仍然需要确定如何拆分整数以获得最大的乘积。动态规划的核心仍然是使用 dp 数组,其中 dp[i] 表示整数 i 被拆分后的最大乘积。

考虑到我们的数学方法,整数3和2是最关键的因素。我们可以进一步优化我们的动态规划解决方案,使其更接近数学解决方案,但仍然使用动态规划的框架。

这是优化后的解决方案:

def integerBreak(n: int) -> int:
    if n == 2: 
        return 1
    if n == 3:
        return 2
    
    # dp[i] 表示数字 i 拆分后的最大乘积
    dp = [0] * (n+1)
    
    # 基本情况
    dp[1], dp[2], dp[3] = 1, 2, 3
    
    for i in range(4, n+1):
        # 因为问题对称,我们只考虑从1到i//2的整数j
        for 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, 1

    for 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) )。

你可能感兴趣的:(算法)