二分数字组合题解

108 阅读8分钟

[原题链接](二分数字组合 - MarsCode) 题目:

二分数字组合

问题描述

小F面临一个有趣的挑战:给定一个数组,她需要将数组中的数字分为两组。分组的目标是使得一组数字的和的个位数等于给定的 A,另一组数字的和的个位数等于给定的 B。除此之外,还有一种特殊情况允许其中一组为空,但剩余数字和的个位数必须等于 A 或 B。小F需要计算所有可能的划分方式。

例如,对于数组 [1, 1, 1] 和目标 A = 1,B = 2,可行的划分包括三种:每个 1 单独作为一组,其余两个 1 形成另一组。如果 A = 3,B = 5,当所有数字加和的个位数为 3 或 5 时,可以有一组为非空,另一组为空。


测试样例

样例1:

输入:n = 3,A = 1,B = 2,array_a = [1, 1, 1]
输出:3

样例2:

输入:n = 3,A = 3,B = 5,array_a = [1, 1, 1]
输出:1

样例3:

输入:n = 2,A = 1,B = 1,array_a = [1, 1]
输出:2

样例4:

输入:n = 5,A = 3,B = 7,array_a = [2, 3, 5, 7, 9]
输出:0

思路:有两种思路一个是用DFS,另一个是用DP(动态规划),我认为dp是更好的,不过dfs也能过可能是数据不强导致的。 先讲动态规划的思路 我们定义一个二维数组 dp[i][j] 表示前 i 个数字能否分成两组,使得一组的和的个位数为 j。我们的目标是计算 dp[n][A] + dp[n][B],其中 n 是数组的长度,AB 是给定的目标个位数。

步骤:

  1. 初始化

    • 创建一个二维数组 dp,大小为 (n+1) x 10,初始化为 False
    • 设置 dp[0][0] = True,因为空数组的和的个位数为 0 是可能的。
  2. 状态转移

    • 遍历数组中的每个数字 x

    • 对于每个可能的和的个位数 k(从 0 到 9),更新 dp[i][j]

      • 如果 dp[i-1][k] 为 True,则设置 dp[i][(k + x) % 10] = True。这表示如果前 i-1 个数字可以分成两组,使得一组的和的个位数为 k,那么加上当前数字 x 后,另一组的和的个位数可以是 (k + x) % 10
      • 同时,如果 k + x 的个位数为 0,则设置 dp[i][0] = True,因为一组的和的个位数为 0 总是可能的。
  3. 计算结果

    • 最后,计算 dp[n][A] + dp[n][B] 来得到所有可能的划分方式的数量。

示例:

对于样例1:n = 3, A = 1, B = 2, array_a = [1, 1, 1]

  • 初始化 dp 为 False,除了 dp[0][0] = True

  • 遍历数字,更新 dp

    • 对于第一个 1:dp[1][1] = True(因为 0 + 1 = 1)。
    • 对于第二个 1:dp[2][2] = True(因为 1 + 1 = 2),dp[2][0] = True(因为 1 + 1 = 2 的个位数是 2,加上 0 还是 2)。
    • 对于第三个 1:dp[3][3] = True(因为 2 + 1 = 3),dp[3][1] = True(因为 0 + 1 = 1),dp[3][0] = True(因为 2 + 1 = 3 的个位数是 3,加上 0 还是 3)。
  • 计算结果:dp[3][1] + dp[3][2] = 2

对于样例2:n = 3, A = 3, B = 5, array_a = [1, 1, 1]

  • 按照上述步骤更新 dp
  • 计算结果:dp[3][3] = True(因为 1 + 1 + 1 = 3),dp[3][5] = False
  • 结果为 1,因为只有一种方式使得一组的和的个位数为 3。

对于样例3:n = 2, A = 1, B = 1, array_a = [1, 1]

  • 按照上述步骤更新 dp
  • 计算结果:dp[2][1] = True(因为 1 + 1 = 2 的个位数是 2,但我们可以有一组为空,另一组的和的个位数为 1)。
  • 结果为 2,因为有两种方式:一组为空,另一组的和的个位数为 1,或者两组都是单个数字 1。

对于样例4:n = 5, A = 3, B = 7, array_a = [2, 3, 5, 7, 9]

  • 按照上述步骤更新 dp
  • 计算结果:dp[5][3] = False 和 dp[5][7] = False
  • 结果为 0,因为没有可能的划分方式使得一组的和的个位数为 3 或 7。

这种方法的时间复杂度是 O(n*10)

DFS思路: 使用深度优先搜索(DFS)解决这个问题的思路是尝试所有可能的数字组合,以确定它们是否可以被分成两组,使得一组的和的个位数等于 A,另一组的和的个位数等于 B。这里的关键点是,我们需要考虑所有可能的子集,并且对于每个子集,我们都需要检查其和的个位数是否符合要求。

步骤:

  1. 定义递归函数

    • 创建一个递归函数 dfs(index, sumA, sumB),其中 index 是当前处理的数组元素的索引,sumA 是当前和的个位数(对应于 A 的目标),sumB 是另一组当前和的个位数(对应于 B 的目标)。
  2. 基本情况

    • 如果 index 等于数组长度,检查 sumA 是否等于 A 或者 sumB 是否等于 B,如果是,则找到了一种有效的划分方式。
  3. 递归探索

    • 对于数组中的每个元素,你有两个选择:要么将其添加到 sumA 的组中,要么将其添加到 sumB 的组中。
    • 更新 sumAsumB 以反映添加当前元素后的新的和的个位数。
    • 递归调用 dfs(index + 1, (sumA + array[index]) % 10, sumB)dfs(index + 1, sumA, (sumB + array[index]) % 10)
  4. 计数

    • 使用一个全局计数器来记录所有有效的划分方式。
  5. 回溯

    • 在递归调用返回后,需要回溯以尝试其他可能的组合。

示例代码框架(伪代码):

function dfs(index, sumA, sumB):
    if index == array.length:
        if sumA == A or sumB == B:
            count++
        return

    dfs(index + 1, (sumA + array[index]) % 10, sumB)
    dfs(index + 1, sumA, (sumB + array[index]) % 10)

count = 0
dfs(0, 0, 0)
return count

注意事项:

  • 这种方法可能会非常慢,因为它尝试了所有可能的组合,时间复杂度是指数级的。
  • 为了避免重复计算,可以使用记忆化技术来存储已经计算过的状态。
  • 由于问题的特殊性,即允许一组为空,因此在基本情况中需要检查 sumA 是否等于 A 或者 sumB 是否等于 B,或者数组的总和的个位数是否等于 A 或 B。

使用 DFS 解决这个问题需要仔细处理递归和回溯,以及如何有效地使用全局计数器来跟踪所有可能的解决方案。这种方法在小规模数据上是可行的,但对于大规模数据,需要我们的第一个更高效的算法,动态规划算法。

DP代码:

def findTargetSumWays(nums, A, B):
    total_sum = sum(nums)
    target_sum_A = (total_sum - B) % 10
    target_sum_B = (total_sum - A) % 10

    # 如果两组的和的个位数之和不能被10整除,则无解
    if (target_sum_A + target_sum_B) % 10 != 0:
        return 0

    # 如果 A 或 B 大于总和的个位数,则无解
    if A > (total_sum % 10) or B > (total_sum % 10):
        return 0

    # 初始化 DP 表,dp[i][j] 表示前 i 个数字能否分成两组,使得一组的和的个位数为 j
    dp = [[0] * 10 for _ in range(len(nums) + 1)]
    dp[0][0] = 1  # 空数组的和的个位数为 0 是可能的

    # 填充 DP 表
    for i in range(1, len(nums) + 1):
        for j in range(10):
            # 不选择当前数字
            if dp[i - 1][j] == 1:
                dp[i][(j + nums[i - 1]) % 10] = 1
            # 选择当前数字
            if j == 0 or dp[i - 1][(j - nums[i - 1]) % 10] == 1:
                dp[i][j] = 1

    # 计算结果
    return dp[-1][A] + dp[-1][B]

# 测试样例
print(findTargetSumWays([1, 1, 1], 1, 2))  # 输出:3
print(findTargetSumWays([1, 1, 1], 3, 5))  # 输出:1
print(findTargetSumWays([1, 1], 1, 1))     # 输出:2
print(findTargetSumWays([2, 3, 5, 7, 9], 3, 7))  # 输出:0

dfs 代码:

def findTargetSumWays(nums, A, B):
    def dfs(index, sumA, sumB):
        # 如果已经处理完所有数字
        if index == len(nums):
            # 如果 A 或 B 的和的个位数等于当前和的个位数,则找到了一种划分方式
            if sumA % 10 == A or sumB % 10 == B:
                return 1
            return 0
        
        # 不包含当前数字,将其分配到 sumB 的组
        not_include = dfs(index + 1, sumA, sumB + nums[index])
        
        # 包含当前数字,将其分配到 sumA 的组
        include = dfs(index + 1, sumA + nums[index], sumB)
        
        # 返回两种情况的和
        return not_include + include

    # 如果总和的个位数小于 A 或 B,则不可能找到解
    total_sum = sum(nums)
    if (total_sum % 10 < A) or (total_sum % 10 < B):
        return 0

    # 如果 A 和 B 的和的个位数不等于总和的个位数,则不可能找到解
    if (A + B) % 10 != total_sum % 10:
        return 0

    # 特殊情况处理:允许一组为空
    if A == 0 or B == 0:
        return 1

    # 从第一个数字开始 DFS
    return dfs(0, 0, 0)

# 测试样例
print(findTargetSumWays([1, 1, 1], 1, 2))  # 输出:3
print(findTargetSumWays([1, 1, 1], 3, 5))  # 输出:1
print(findTargetSumWays([1, 1], 1, 1))     # 输出:2
print(findTargetSumWays([2, 3, 5, 7, 9], 3, 7))  # 输出:0

谢谢大家的阅读,觉得有帮助可以给我点赞支持,嘿嘿。