问题描述
小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 aiai,背面是 bibi。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 109+7109+7 取模。
例如:如果有3张卡牌,正反面数字分别为 (1,2),(2,3) 和 (3,2),你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。
测试样例
示例一:
输入:
n = 3 ,a = [1, 2, 3] ,b = [2, 3, 2]输出:
3
示例二:
输入:
n = 4 ,a = [3, 1, 2, 4] ,b = [1, 2, 3, 1]输出:
6
示例三:
输入:
n = 5 ,a = [1, 2, 3, 4, 5] ,b = [1, 2, 3, 4, 5]输出:
32
解题思路
-
问题建模:
- 将问题转化为一个组合问题,即从n张卡牌中选择若干张,使得这些卡牌的数字之和能被3整除。
-
状态定义:
dp[i][j]表示在考虑前i张卡牌的情况下,当前选择的卡牌数字之和模3等于j的方案数。
-
状态初始化:
dp[0][0] = 1表示在没有卡牌时,和为0的方案数为1,这是一个基本情况。
-
状态转移:
- 对于每张卡牌,我们有两种选择:选择正面或背面。这意味着我们需要更新
dp数组,以反映这两种选择对总和模3的影响。 - 对于第
i张卡牌,如果选择正面,那么dp[i][(j + a[i - 1]) % 3]需要加上dp[i - 1][j],因为a[i - 1]是第i张卡牌正面的数字。 - 同样,如果选择背面,那么
dp[i][(j + b[i - 1]) % 3]也需要加上dp[i - 1][j],因为b[i - 1]是第i张卡牌背面的数字。
- 对于每张卡牌,我们有两种选择:选择正面或背面。这意味着我们需要更新
-
遍历卡牌:
- 外层循环遍历每张卡牌,内层循环遍历所有可能的模3结果(0, 1, 2)。
-
结果提取:
- 最终结果为
dp[n][0],即考虑所有卡牌后,和模3为0的方案数。
- 最终结果为
核心代码
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
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
# 结果是所有选择的和模3为0的方案数
return dp[n][0]
if __name__ == '__main__':
print(solution(n = 3, a = [1, 2, 3], b = [2, 3, 2]) == 3)
print(solution(n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1]) == 6)
print(solution(n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5]) == 32)
代码分析
-
MOD = 10**9 + 7:定义了一个大素数作为模数,用于在计算过程中防止整数溢出。 -
dp = [[0] * 3 for _ in range(n + 1)]:创建了一个二维数组,用于存储动态规划的状态。数组的行数为n + 1,列数为3,对应于模3的三种可能结果。 -
dp[0][0] = 1:初始化了动态规划数组,表示在没有卡牌时,和为0的方案数为1。 -
双层循环:外层循环遍历每张卡牌,内层循环遍历所有可能的模3结果。
-
dp[i][(j + a[i - 1]) % 3] = (dp[i][(j + a[i - 1]) % 3] + dp[i - 1][j]) % MOD:更新dp数组,考虑选择当前卡牌的正面。 -
dp[i][(j + b[i - 1]) % 3] = (dp[i][(j + b[i - 1]) % 3] + dp[i - 1][j]) % MOD:更新dp数组,考虑选择当前卡牌的背面。 -
return dp[n][0]:返回最终结果,即考虑所有卡牌后,和模3为0的方案数。
核心知识
-
动态规划(Dynamic Programming, DP) :
- 动态规划是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。
- 它将问题分解为重叠的子问题,通过解决子问题来构建原问题的解。
-
组合数学(Combinatorics) :
- 组合数学是数学的一个分支,主要研究可数或离散对象的科学,如排列、组合等。
-
模运算(Modular Arithmetic) :
- 模运算是整数运算的一种形式,其中数字是以一个称为模数的固定正整数为模进行“包裹”的。
- 在本题中,使用模运算可以避免大数问题,因为直接计算可能会得到非常大的数字。
-
状态转移方程(State Transition Equation) :
- 状态转移方程是动态规划中用于从一个状态转移到另一个状态的数学表达式。
- 在本题中,状态转移方程是
dp[i][(j + a[i - 1]) % 3]和dp[i][(j + b[i - 1]) % 3]。
-
优化技巧:
- 使用模数
10^9 + 7来避免大数问题,这是一种常见的优化手段。
- 使用模数
总结
这个问题考察了动态规划和模运算的应用。通过定义合适的状态和状态转移方程,我们可以有效地解决这个问题。关键在于理解如何将问题分解为更小的子问题,并通过动态规划的方法来避免重复计算。此外,模运算的使用是解决大数问题的一种常见技巧。
使用豆包marscode AI的优势
-
免费使用:豆包MarsCode是一个免费的AI编程助手,与一些收费的编程辅助工具相比,这一点非常吸引人
-
代码补全与推荐:提供智能补全,不仅限于单词或简单句子的补全,而是能够根据上下文和编程习惯提供整行代码或代码块的建议。
-
代码优化与重构:自动检测并建议更高效、更简洁的代码写法,帮助开发者重构代码,提高代码的可读性和可维护性。
-
错误检测与修复:在编写代码时即时指出潜在的错误或bug,并提供修复建议,甚至能够自动修复一些常见错误。