题目解析
问题描述
小M有 (n) 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 (a_i),背面是 (b_i)。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被 3 整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 (10^9+7) 取模。
我们可以通过组合每张卡牌正反面朝上的数字来实现目标。我们需要找到满足条件的所有组合数,具体是如何选择正反面使得总和能被 3 整除。
思路解析
-
动态规划(DP):
- 我们可以使用动态规划来解决这个问题。设 (dp[r]) 表示当前数字的和除以 3 的余数是 (r) 的方案数。这里 (r) 的取值为 0, 1, 2,表示当前和对 3 取余后可能的三种余数。
-
状态转移:
- 对于每张卡牌,我们可以选择它的正面或者背面。正面和背面的数字分别是 (a_i) 和 (b_i),我们需要根据当前的和的余数来计算新的状态。
- 假设当前余数为 (r),对于正面朝上的卡牌,我们更新下一个状态为 ((r + a_i) % 3),对于背面朝上的卡牌,我们更新下一个状态为 ((r + b_i) % 3)。
-
初始化:
- 初始化时,只有 (dp[0] = 1),表示初始状态为数字和为 0(即符合条件的初始状态),而 (dp[1]) 和 (dp[2]) 为 0,表示初始时不可能有余数为 1 或 2 的情况。
-
更新:
- 对每张卡牌,更新 (dp) 数组,以确保计算所有可能的组合。
代码详解
def solution(n: int, a: list, b: list) -> int:
MOD = 10**9 + 7 # 结果需要对 10^9+7 取模
# 初始化 DP 数组,只有 dp[0] = 1,其余为 0
dp = [1, 0, 0] # dp[i] 表示当前和对 3 取余为 i 的方案数
# 遍历每张卡牌
for i in range(n):
next_dp = [0] * 3 # 用来存储更新后的状态
for r in range(3):
# 更新正面朝上的情况
next_dp[(r + a[i]) % 3] += dp[r]
next_dp[(r + a[i]) % 3] %= MOD # 保证取模
# 更新背面朝上的情况
next_dp[(r + b[i]) % 3] += dp[r]
next_dp[(r + b[i]) % 3] %= MOD # 保证取模
dp = next_dp # 更新 dp 数组为新状态
# 最终结果是 dp[0],表示所有方案中,数字和对 3 取余为 0 的方案数
return dp[0]
代码解析
-
初始化:
dp = [1, 0, 0]表示初始时,和为 0 的情况有 1 种,和为 1 和 2 的情况没有。 -
循环遍历:
for i in range(n):遍历每一张卡牌。next_dp = [0] * 3:创建一个新的数组next_dp来存储更新后的状态。
-
状态转移:
- 对于当前状态 (r)(即当前的和对 3 取余的结果),我们尝试选择每张卡牌的正面或背面:
next_dp[(r + a[i]) % 3] += dp[r]:表示当前状态为 (r),选择正面 (a_i) 后的新状态是 ((r + a[i]) % 3)。next_dp[(r + b[i]) % 3] += dp[r]:表示当前状态为 (r),选择背面 (b_i) 后的新状态是 ((r + b[i]) % 3)。
- 对于当前状态 (r)(即当前的和对 3 取余的结果),我们尝试选择每张卡牌的正面或背面:
-
更新 dp:
dp = next_dp表示将dp数组更新为新的状态数组。 -
结果返回:
return dp[0]表示最终和对 3 取余为 0 的方案数。
知识总结
-
动态规划(DP):本题是一个典型的动态规划问题,通过状态转移来求解问题。利用 DP 技巧,我们能够通过状态压缩和重复计算的消除,显著提高计算效率。
-
取模运算:在题目中要求结果对 (10^9+7) 取模,这是为了防止数值溢出,并且也是常见的编程竞赛中的常规要求。使用取模操作,能够保证程序在大数情况下依然能够稳定运行。
-
问题建模与解法:通过分析问题,将其转化为一个动态规划问题,并设计出合理的状态转移方程,从而高效地求解。