学习笔记:选择卡牌面使得数字和能被3整除
题目解析
题目要求我们从多张卡牌中选择每张卡牌的正面或背面,使得所选的卡牌上数字之和能够被3整除。具体而言,对于每张卡牌,我们可以选择它的正面或背面,所选择的数字之和对3的余数必须是0。
在这个问题中,给定的正面和背面数字都很可能是任意值,如何在不超时的情况下得到所有可能方案的数量,成了解决问题的关键。
解题思路
我们需要用到动态规划(DP)来逐步计算出所有可能的方案。 状态定义: 我们定义一个DP数组 dp[i][j],表示前i张卡牌中,和对3取余为j的方案数。其中,j 的取值范围是0, 1, 2,因为一个数对3的余数只能是这三个值之一。 初始化: 对于0张卡牌的情况,和为0的方案数为1,dp[0][0] = 1。这表示在没有卡牌的情况下,和为0的方案是唯一的。 状态转移: 对于每一张卡牌,我们有两种选择:选择正面数字或选择背面数字。假设卡牌的正面数字是 a[i],背面数字是 b[i],那么: 如果前i-1张卡牌的和对3余j,选择正面时,新的余数是 (j + a[i]) % 3。 如果前i-1张卡牌的和对3余j,选择背面时,新的余数是 (j + b[i]) % 3。 通过递归的方式,逐步更新DP数组。 最终目标: 我们需要返回在考虑了所有卡牌之后,和对3取余为0的方案数,即 dp[n][0]。
代码实现
以下是完整的代码,包含详细注释:
def solution(n, a, b):
# 定义dp数组,dp[i][j]表示前i张卡牌,和模3余j的方案数
dp = [[0] * 3 for _ in range(n + 1)]
# 初始化:前0张卡牌,和为0的方案数为1
dp[0][0] = 1
# 遍历每一张卡牌
for i in range(1, n + 1):
# 当前卡牌的正面和背面数字
front = a[i - 1]
back = b[i - 1]
# 更新 dp 数组
for j in range(3):
# 选择正面的情况
dp[i][(j + front) % 3] = (dp[i][(j + front) % 3] + dp[i - 1][j]) % MOD
# 选择背面的情况
dp[i][(j + back) % 3] = (dp[i][(j + back) % 3] + dp[i - 1][j]) % MOD
# 返回前 n 张卡牌中,和模3余0的方案数
return dp[n][0]
# 测试样例
print(solution(3, [1, 2, 3], [2, 3, 2])) # 输出应为 3
print(solution(4, [3, 1, 2, 4], [1, 2, 3, 1])) # 输出应为 6
print(solution(5, [1, 2, 3, 4, 5], [1, 2, 3, 4, 5])) # 输出应为 32
学习与思考
- 动态规划思想 这个问题通过动态规划逐步构建解决方案,主要通过“状态转移”的思想来更新当前的状态。我们从无卡牌的状态(和为0)开始,一张一张卡牌地更新,直到最后获得符合要求的方案数。
- 时间复杂度 每一张卡牌的状态更新操作涉及到三种余数(0, 1, 2),因此对于每张卡牌的处理是常数时间操作。总体的时间复杂度为 O(n^2),其中 n 是卡牌的数量。这个时间复杂度对于大多数输入规模来说是足够快的。
- 模运算的使用 由于结果可能会非常大,我们使用了取模 10^9 +7 来确保结果不会溢出,并且在计算过程中每一步都对结果进行取模操作,保证了数值的正确性。
总结
理解动态规划的基本思想:解决问题的关键是将问题拆解为子问题,并通过状态转移逐步求解。 注意边界条件和状态初始化:无论是卡牌问题还是其他类型的问题,动态规划的状态初始化非常重要,必须确保每个状态有合理的初值。 不断练习:动态规划虽然理论上清晰,但在实际操作中经常会遇到复杂的转移关系,练习和总结是提高的关键。