问题
题目列表第33道:卡牌翻面求和问题,难度为难
问题描述
小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 ai,背面是 bi。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对10^9+7取模。
例如:如果有3张卡牌,正反面数字分别为 (1,2),(2,3) 和 (3,2),你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。
测试样例
样例1:
输入:
n = 3 ,a = [1, 2, 3] ,b = [2, 3, 2]
输出:3
样例2:
输入:
n = 4 ,a = [3, 1, 2, 4] ,b = [1, 2, 3, 1]
输出:6
样例3:
输入:
n = 5 ,a = [1, 2, 3, 4, 5] ,b = [1, 2, 3, 4, 5]
输出:32
解题思路
这个问题也是求组合的问题,我们很容易就能想到动态规划。动态规划的思想最初是由美国数学家贝尔曼在1951提出,它背后的思想是最优化定理。从算法的角度来看,什么时候可以使用动态规范方法解决问题呢?有两个条件,即 ”重叠子结构“ 和 ”最优子结构“。 前者是指原问题能够分解为几个相互联系的单阶段问题,且一定是同一类型的子问题。必须利用前面子结构计算出的结果,用递推不断地调用同一个问题。而“最优子结构”的意思是全局最优解一定能够拆成子问题的最优解,换句话说,整体问题的最优解一定包含子问题的最优解。不难看出,我们现在求解的问题符合这两个使用条件,也就可以使用动态规划来做。
动态规划的重点在于数组含义的定义,写出状态转移方程以及边界条件的设定。在数组含义上,我们可以只用二维数组 dp[i][j] 表示考虑前 i 张卡片时,总和模3的结果为 j 的方案数。
- 状态转移方程
对于每一张卡片 i,我们有两种选择:选择正面或选择背面,因此需要不断更新 dp 数组以反映这两种选择对总和模3结果的影响。假设第 i张卡片正面数字和反面数字分别为a[i]和b[i],那么dp[i][j] 就可以更新为 dp[i-1][(j - a[i]) % 3] + dp[i-1][(j - b[i]) % 3],其中 (j - a[i]) % 3 和 (j - b[i]) % 3 分别表示选择正面和背面后对总和模3的影响。注意到 j 可能为负数,因此我们需要调整模运算的结果,确保其在 [0, 2] 范围内。
- 边界条件
初始化 dp[0][0] = 1,表示没有卡片时总和为0的方案数为1,其他 dp[0][j] 初始化为0。
最终,我们求的是 dp[n][0],即考虑所有卡片后,总和模3的结果为0(也就是能整除3)的方案数。
具体实现
思路阐明后,代码的实现还是比较简单的。动态规划的思路大致可以分为初始化dp数组,初始化边界条件,填充dp数组这几步。完整代码如下。
MOD = 10**9+7
dp = [[0] * 3 for _ in range(n + 1)]
dp[0][0] = 1
# 填充 dp 数组
for i in range(1, n + 1):
for j in range(3):
# 计算选择正面和背面后的模3结果
mod_a = (j - a[i-1]) % 3
mod_b = (j - b[i-1]) % 3
# 更新 dp 数组
dp[i][j] = (dp[i-1][mod_a] + dp[i-1][mod_b]) % MOD
# 返回结果
return dp[n][0]
结语
通过这道题,我们对动态规划算法的使用条件,写法和原理有了更加深刻的认知,希望以后能逐步挑战更多更难的题目。