问题描述
小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 aiai,背面是 bibi。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 109+7109+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
问题理解
这个问题是一个典型的动态规划问题,涉及到对每张卡牌的两种选择(正面或背面)进行状态转移。我们需要计算所有可能的和模3的结果的数量,最终找到和为0的方案数。
数据结构选择
我们可以使用动态规划来解决这个问题。具体来说,我们可以使用一个数组 dp 来记录当前所有可能的和模3的结果的数量。动态规划的核心思想是将问题分解为子问题,并通过状态转移方程逐步求解。在这个问题中,我们使用一个长度为3的数组 dp 来记录当前所有可能的和模3的结果的数量。通过遍历每张卡牌,我们更新 dp 数组,最终得到所有卡牌正面或背面朝上的数字之和可以被3整除的组合数。
算法步骤
- 初始化:创建一个长度为3的数组
dp,初始值为[1, 0, 0],表示和为0的方案数为1,和为1和2的方案数为0。 - 状态转移:对于每一张卡牌,我们更新
dp数组。对于每张卡牌的正面和背面数字,我们分别计算它们对当前dp数组的影响。 - 结果:最终,
dp[0]就是所有卡牌正面或背面朝上的数字之和可以被3整除的组合数。
具体代码
public class Main {
public static int solution(int n, int[] a, int[] b) {
final int MOD = 1000000007;
int[] dp = new int[3];
dp[0] = 1; // 初始状态,和为0的方案数为1
for (int i = 0; i < n; i++) {
int[] newDp = new int[3];
// 计算当前卡牌正面和背面的影响
int aMod = a[i] % 3;
int bMod = b[i] % 3;
// 更新newDp数组
for (int j = 0; j < 3; j++) {
newDp[(j + aMod) % 3] = (newDp[(j + aMod) % 3] + dp[j]) % MOD;
newDp[(j + bMod) % 3] = (newDp[(j + bMod) % 3] + dp[j]) % MOD;
}
// 更新dp数组
dp = newDp;
}
return dp[0]; // 返回和为0的方案数
}
public static void main(String[] args) {
System.out.println(solution(3, new int[]{1, 2, 3}, new int[]{2, 3, 2}) == 3);
System.out.println(solution(4, new int[]{3, 1, 2, 4}, new int[]{1, 2, 3, 1}) == 6);
System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 32);
}
}
动态规划解决卡牌问题的关键步骤
1. 初始化
• 状态定义:首先,我们需要定义一个状态数组dp,其中dp[i]表示当前所有卡牌正面或背面数字之和模3等于i的方案数。由于我们关心的是能否被3整除,所以只需要考虑i为0、1、2的情况。
• 初始值:由于初始状态下没有任何卡牌被选中,所以和为0的方案数为1(即什么都不选),因此dp[0] = 1。而dp[1]和dp[2]则初始化为0,表示初始状态下不存在其他和模3等于1或2的方案。
2. 状态转移
• 遍历卡牌:对于每张卡牌,我们有两个选择:选正面或选背面。
• 计算影响:选择卡牌的正面或背面后,我们需要计算其对当前dp数组的影响。具体来说,如果当前卡牌的正面数字为a,背面数字为b,则选择正面后,我们需要将dp[i]的值加到newDp[(i + a) % 3]上;选择背面后,我们需要将dp[i]的值加到newDp[(i + b) % 3]上。
• 新数组:遍历完所有卡牌后,我们使用newDp数组的值更新原始的dp数组,为下一轮迭代做准备。
3. 结果输出
• 最终,dp[0]的值就是所有卡牌正面或背面朝上的数字之和可以被3整除的组合数。
学习建议
1. 理解动态规划基础
• 状态定义:明确每个状态的含义,以及状态之间的转移关系。
• 状态转移方程:理解并推导状态转移方程,这是动态规划的核心。
• 边界条件:注意初始状态的设定,以及特殊情况的处理。
2. 制定刷题计划
• 分阶段刷题:从简单到复杂,逐步提升难度。可以先从基础的动态规划问题开始,例如斐波那契数列、背包问题等。
• 定期复习:定期回顾已经解决的问题,加深理解。可以使用AI刷题功能中的错题本功能,记录并复习错题。
• 模拟考试:定期进行模拟考试,检验自己的学习效果。AI刷题功能可以提供模拟考试环境,帮助你熟悉考试节奏。
3. 利用错题进行针对性学习
• 分析错题:对于每道错题,分析错误原因,是理解问题不透彻,还是代码实现有误。
• 总结规律:总结常见错误类型和解决方法,形成自己的错题本。
• 针对性练习:针对薄弱环节进行针对性练习,例如动态规划中的状态转移方程理解不透彻,可以多做一些相关题目。
工具运用
1. 结合AI刷题功能
• 智能推荐:利用AI刷题功能的智能推荐功能,根据你的学习进度和错题情况,推荐适合的题目。
• 实时反馈:在刷题过程中,利用AI的实时反馈功能,及时发现并纠正错误。
• 错题本:使用错题本功能,记录并复习错题,形成自己的错题库。
2. 结合其他学习资源
• 在线课程:结合在线课程学习,例如Coursera、edX、LeetCode等平台上的算法课程,系统学习算法和数据结构。
• 书籍阅读:阅读经典算法书籍,例如《算法导论》、《深入理解计算机系统》等,深入理解算法原理和系统底层知识。
• 社区交流:参与算法学习社区,例如CSDN、GitHub、LeetCode讨论区、Stack Overflow等,与其他学习者交流经验,解决问题。同时,可以关注一些算法博客或公众号,获取最新的算法学习资料和行业动态。