心得笔记:基于动态规划解决数字组合问题
前言
组合问题是算法学习中的一个经典课题,尤其是在涉及数字分组和特定约束条件时。本题要求从多个数字组中选择数字组成新的数,并使其各位数字之和为偶数。通过分析问题并引入动态规划的思想,我们可以高效解决这一复杂问题。以下是对问题、解决方案以及实现过程中重要思想的总结。
问题分析
题目背景
我们面对多个数字组,每个组中包含若干个数字。从每组中选择一个数字组成新的数,目标是使新数的各位数字之和为偶数。任务是计算所有可能符合条件的组合数。
问题特点
- 数字分组:每组数字是一个独立的选择空间。
- 和的奇偶性:目标约束是数字之和为偶数,这对数字选择有直接影响。
- 组合问题:涉及多个选择,暴力枚举的复杂度较高,需要优化。
输入输出
输入:
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