基于动态规划解决数字组合问题 | 豆包MarsCode AI刷题

心得笔记:基于动态规划解决数字组合问题

前言

组合问题是算法学习中的一个经典课题,尤其是在涉及数字分组和特定约束条件时。本题要求从多个数字组中选择数字组成新的数,并使其各位数字之和为偶数。通过分析问题并引入动态规划的思想,我们可以高效解决这一复杂问题。以下是对问题、解决方案以及实现过程中重要思想的总结。


问题分析

题目背景

我们面对多个数字组,每个组中包含若干个数字。从每组中选择一个数字组成新的数,目标是使新数的各位数字之和为偶数。任务是计算所有可能符合条件的组合数。

问题特点
  1. 数字分组:每组数字是一个独立的选择空间。
  2. 和的奇偶性:目标约束是数字之和为偶数,这对数字选择有直接影响。
  3. 组合问题:涉及多个选择,暴力枚举的复杂度较高,需要优化。
输入输出

输入

  • numbers:一个列表,每个元素是字符串形式的数字组。

输出

  • 返回所有符合条件的组合数。
示例分析

示例1

  • 输入:numbers = [123, 456, 789]

  • 每组数字拆解后为:

    • 第一组:[1, 2, 3]
    • 第二组:[4, 5, 6]
    • 第三组:[7, 8, 9]
  • 目标:从每组中选择一个数字,使数字之和为偶数。

  • 符合条件的组合数为 14

示例2

  • 输入:numbers = [123456789]
  • 仅有一组数字:[1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 条件:选择一个数字,需使其为偶数。
  • 符合条件的组合数为 4(偶数为 [2, 4, 6, 8])。

解决思路

1. 动态规划的引入

考虑到问题的组合性以及逐组选择的性质,我们可以使用动态规划来解决:

  • 状态定义

    • dp[i][0]:表示在前 i 组数字中选择的组合,使和为偶数的方案数。
    • dp[i][1]:表示在前 i 组数字中选择的组合,使和为奇数的方案数。
  • 状态转移

    • 从上一组状态转移到当前组时,若选择一个数字:

      • 如果该数字为偶数,奇偶性保持不变。
      • 如果该数字为奇数,奇偶性翻转。
  • 初始化

    • dp[0][0] = 1,表示未选择任何数字时,总和为偶数的初始方案数为 1。
2. 转移公式

对于每组数字中的每个数字,根据奇偶性对 dp 数组进行更新:

  • 若数字 num 为偶数:

    • dp[i+1][0] += dp[i][0](偶数方案保持偶数)
    • dp[i+1][1] += dp[i][1](奇数方案保持奇数)
  • 若数字 num 为奇数:

    • dp[i+1][0] += dp[i][1](奇数方案转为偶数)
    • dp[i+1][1] += dp[i][0](偶数方案转为奇数)
3. 最终目标

遍历完所有数字组后,dp[len(numbers)][0] 即为所有满足和为偶数的方案数。


代码实现

以下是基于动态规划的完整实现:

python
复制代码
from collections import defaultdict

def solution(numbers):
    # 初始化 dp 数组,dp[i][0] 表示和为偶数的组合数,dp[i][1] 表示和为奇数的组合数
    dp = [[0, 0] for _ in range(len(numbers) + 1)]
    dp[0][0] = 1  # 初始状态:未选择任何数字时,总和为0(偶数)

    # 遍历每一个数字组
    for i, group in enumerate(numbers):
        # 临时数组用于存储当前组的计算结果,避免覆盖 dp[i]
        temp = [[0, 0] for _ in range(2)]

        # 遍历当前组中的每一个数字
        for num in str(group):
            num = int(num)  # 将字符转换为整数
            if num % 2 == 0:
                # 数字为偶数时:状态保持不变
                temp[0][0] += dp[i][0]
                temp[1][1] += dp[i][1]
            else:
                # 数字为奇数时:状态翻转
                temp[0][1] += dp[i][0]
                temp[1][0] += dp[i][1]

        # 更新 dp 数组
        dp[i + 1][0] = temp[0][0] + temp[1][0]
        dp[i + 1][1] = temp[0][1] + temp[1][1]

    # 返回所有组合中,和为偶数的组合数
    return dp[len(numbers)][0]

# 测试代码
if __name__ == "__main__":
    print(solution([123, 456, 789]) == 14)
    print(solution([123456789]) == 4)
    print(solution([14329, 7568]) == 10)

复杂度分析

1. 时间复杂度
  • 外层循环遍历 len(numbers) 个数字组。
  • 内层循环遍历每组的所有数字,假设平均长度为 k
  • 总体复杂度为 O(n * k) ,其中 n 是组数,k 是每组的平均长度。
2. 空间复杂度
  • 动态规划数组 dp 的大小为 O(n)

学习与收获

1. 动态规划的核心思想

动态规划的关键在于将问题分解为多个子问题,逐步求解。本题中,通过维护和更新奇偶性状态,我们可以高效地解决问题。

2. 状态定义的技巧

奇偶性是一种简单但强大的状态分类方式。通过将奇数和偶数的组合分别记录,可以清晰地表达状态转移过程。

3. 灵活运用数组操作

在更新状态时,使用临时数组避免直接覆盖原始数据,确保状态转移过程的正确性。


问题拓展

1. 更多约束条件

如果增加其他约束(如结果需要为特定的模数),可以扩展状态定义,将模数余数作为状态的一部分。

2. 数字组之间的依赖性

若不同组之间有依赖关系(如顺序限制),动态规划的转移公式需要根据具体依赖关系调整。

3. 并行计算优化

在大规模数据下,可以通过并行化动态规划的计算过程进一步提升效率。


总结

本题通过动态规划的思想有效解决了一个复杂的组合问题。它不仅考察了对动态规划的理解,还培养了通过状态分类解决问题的思维能力。通过这次学习,我深刻体会到算法设计中的细致与巧妙,未来也会将这些技巧运用到更广泛的实际问题中。

4o