卡牌翻面求和问题
一、问题描述
小M拥有 ( n ) 张卡牌,每张卡牌的正反面分别写着不同的数字,正面为 ( a_i ),背面为 ( b_i )。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被 3 整除。我们的任务是计算出所有满足该条件的方案数量,并对 ( 10^9 + 7 ) 取模。
例如,若有 3 张卡牌,正反面数字分别为 (1, 2),(2, 3) 和 (3, 2),我们需要找到所有满足这 3 张卡牌正面或背面朝上的数字之和可以被 3 整除的组合数。
二、思路解析
这个问题可以通过动态规划来解决。我们需要记录在选择前 ( i ) 张卡牌的情况下,所有可能的和对 3 取模的方案数。动态规划的状态转移可以通过以下步骤进行:
-
状态定义:
- 设 ( dp[i][j] ) 为前 ( i ) 张卡牌中,和对 3 取模为 ( j ) 的方案数,其中 ( j ) 的取值为 0, 1, 2(分别表示和对 3 取模的结果)。
-
状态转移:
- 对于每张卡牌 ( i ),我们可以选择正面 ( a[i-1] ) 或背面 ( b[i-1] )。
- 如果选择正面 ( a[i-1] ),则和对 3 取模的值会变为 ( (j + a[i-1]) \mod 3 )。
- 如果选择背面 ( b[i-1] ),则和对 3 取模的值会变为 ( (j + b[i-1]) \mod 3 )。
- 因此,状态转移方程可以表示为:
- ( dp[i][(j + a[i-1]) \mod 3] += dp[i-1][j] )
- ( dp[i][(j + b[i-1]) \mod 3] += dp[i-1][j] )
-
初始状态:
- ( dp[0][0] = 1 ):表示没有卡牌时,和为 0 的方案数为 1。
-
计算结果:
- 最终结果为 ( dp[n][0] ),即前 ( n ) 张卡牌中,和对 3 取模为 0 的方案数量。
三、步骤
- 初始化动态规划数组 ( dp )。
- 设置初始状态 ( dp[0][0] = 1 )。
- 遍历每张卡牌,更新 ( dp ) 数组。
- 返回 ( dp[n][0] ) 的值。
四、图解
假设我们有 3 张卡牌,正反面数字为:
- 卡牌 1: ( a_1 = 1, b_1 = 2 )
- 卡牌 2: ( a_2 = 2, b_2 = 3 )
- 卡牌 3: ( a_3 = 3, b_3 = 2 )
在动态规划过程中,状态转移如下:
- 初始状态 ( dp[0] = [1, 0, 0] )
- 选择卡牌 1:
- 选择正面 ( a_1 ):( dp[1][1] += dp[0][0] ) → ( dp[1] = [0, 1, 0] )
- 选择背面 ( b_1 ):( dp[1][2] += dp[0][0] ) → ( dp[1] = [0, 1, 1] )
- 选择卡牌 2:
- 选择正面 ( a_2 ):( dp[2][0] += dp[1][1] ) → ( dp[2] = [1, 1, 1] )
- 选择背面 ( b_2 ):( dp[2][1] += dp[1][1] ) → ( dp[2] = [1, 2, 1] )
- 选择卡牌 3:
- 选择正面 ( a_3 ):( dp[3][0] += dp[2][2] ) → ( dp[3] = [1, 2, 2] )
- 选择背面 ( b_3 ):( dp[3][2] += dp[2][1] ) → ( dp[3] = [1, 2, 4] )
最终,( dp[3][0] = 3 ),表示有 3 种方案使得和对 3 取模为 0。
五、代码详解
以下是实现上述逻辑的 Python 代码:
def solution(n: int, a: list, b: list) -> int:
MOD = 10**9 + 7
# dp[i][j] 表示前 i 张卡牌中,和对 3 取模为 j 的方案数
dp = [[0] * 3 for _ in range(n + 1)]
dp[0][0] = 1 # 初始状态,和为 0 的方案数为 1
for i in range(1, n + 1):
for j in range(3):
# 选择正面
dp[i][(j + a[i - 1]) % 3] = (dp[i][(j + a[i - 1]) % 3] + dp[i - 1][j]) % MOD
# 选择背面
dp[i][(j + b[i - 1]) % 3] = (dp[i][(j + b[i - 1]) % 3] + dp[i - 1][j]) % MOD
return dp[n][0]
if __name__ == '__main__':
print(solution(n = 3, a = [1, 2, 3], b = [2, 3, 2]) == 3) # 输出: True
print(solution(n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1]) == 6) # 输出: True
print(solution(n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5]) == 32) # 输出: True
代码解读
-
初始化:
dp = [[0] * 3 for _ in range(n + 1)]创建一个大小为 (n+1)×3 的数组,初始化为 0。dp[0][0] = 1表示没有卡牌时,和为 0 的方案数为 1。
-
状态转移:
- 外层循环遍历每张卡牌,内层循环遍历和对 3 取模的结果。
- 通过选择正面和背面,更新当前状态。
-
返回结果:
return dp[n][0]返回最后的结果,即前 ( n ) 张卡牌中,和对 3 取模为 0 的方案数量。
六、个人总结
在解决卡牌翻面求和问题的过程中,我深刻体会到了动态规划的强大和灵活性。这个问题表面上看起来简单,但通过合理的状态定义和转移,可以有效地将复杂度降低到线性级别。
在实现过程中,我注意到动态规划的状态转移需要细心处理,确保每一步的更新都是基于前一步的结果。通过对状态的合理划分,我们能够清晰地理解每张卡牌对最终结果的影响。
此外,我也体会到代码的可读性和注释的重要性。在复杂的逻辑中,清晰的注释可以帮助我和他人更快地理解代码的意图和实现方式。未来在编写代码时,我会更加注重这些方面。
通过这个问题的学习,我对动态规划的理解更加深入,也增强了我解决实际问题的能力。接下来,我会继续练习更多的动态规划问题,以巩固我的学习成果,并探索更复杂的算法和数据结构。