问题描述
小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 aiai,背面是 bibi。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 109+7109+7 取模。
思路分析
-
问题本质:
- 我们需要找到一种方法,使得 nn 张卡牌的正面或背面朝上的数字之和可以被3整除。
- 每张卡牌有2种选择(正面或背面),因此总共有 2n2n 种不同的组合方式。
-
动态规划:
- 使用动态规划(Dynamic Programming, DP)来解决这个问题。定义一个二维数组
dp,其中dp[i][j]表示前i张卡牌的数字之和模3等于j的方案数。 - 初始化
dp[0][0] = 1,表示没有卡牌时,数字之和模3为0的方案数为1(空集)。 - 遍历每张卡牌,更新
dp数组。
- 使用动态规划(Dynamic Programming, DP)来解决这个问题。定义一个二维数组
-
状态转移方程:
-
对于第
i张卡牌,假设其正面数字为front,背面数字为back。 -
更新
dp数组的公式为:
-
dp[i][(j+front)%3]=dp[i][(j+front)%3]+dp[i-1][j]%MOD;
dp[i][(j+back)%3]=dp[i][(j+back)%3]+dp[i-1][j]%MOD;
代码详解
public class Main {
public static int solution(int n, int[] a, int[] b) {
final int MOD = 100000007;
int[][] dp = new int[n + 1][3];
// 初始化dp数组
dp[0][0] = 1;
// 遍历每张卡牌
for (int i = 1; i <= n; i++) {
int front = a[i - 1];
int back = b[i - 1];
// 遍历前一张卡牌的三种状态
for (int j = 0; j < 3; j++) {
dp[i][(j + front) % 3] = (dp[i][(j + front) % 3] + dp[i - 1][j]) % MOD;
dp[i][(j + back) % 3] = (dp[i][(j + back) % 3] + dp[i - 1][j]) % MOD;
}
}
// 返回结果
return dp[n][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(3, new int[]{1, 2, 3}, new int[]{2, 3, 2}) == 3); // 输出 true
System.out.println(solution(4, new int[]{3, 1, 2, 4}, new int[]{1, 2, 3, 1}) == 6); // 输出 true
System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 32); // 输出 true
}
}
知识总结
-
动态规划:
- 动态规划是一种通过将问题分解成子问题来解决复杂问题的方法。在这个问题中,我们使用了一个二维数组
dp来记录每张卡牌的处理状态。 - 初始化
dp[0][0] = 1,表示没有卡牌时的初始状态。 - 状态转移方程
dp[i][(j + front) % 3]和dp[i][(j + back) % 3]用于更新每张卡牌的处理结果。
- 动态规划是一种通过将问题分解成子问题来解决复杂问题的方法。在这个问题中,我们使用了一个二维数组
-
模运算:
- 由于结果可能非常大,我们需要对 109+7109+7 取模。取模运算可以防止整数溢出,并且保持计算结果在合理范围内。
- 在更新
dp数组时,每次累加结果后都需要取模。
-
数组处理:
- 使用
for循环遍历数组中的每一个元素,处理每张卡牌。 - 通过
a[i - 1]和b[i - 1]分别获取每张卡牌的正面和背面数字。
- 使用
-
边界条件:
- 考虑边界条件,确保数组索引不越界。例如,处理第
i张卡牌时,数组索引为i - 1。
- 考虑边界条件,确保数组索引不越界。例如,处理第
个人理解与学习建议
-
问题简化:
- 这个问题的核心是找到一种方法,使得所有卡牌的数字之和模3为0。通过动态规划,我们可以有效地解决这个问题。
- 动态规划的状态转移方程是解决此类问题的关键,需要仔细理解和推导。
-
代码可读性:
- 在编写代码时,尽量使用有意义的变量名,如
front和back,这样可以使代码更易读。 - 添加注释,特别是对于复杂逻辑的部分,可以提高代码的可维护性。
- 在编写代码时,尽量使用有意义的变量名,如
-
学习建议:
- 理解动态规划:动态规划是一种重要的算法思路,有助于解决很多复杂问题。通过这个题目,可以加深对动态规划的理解。
- 掌握模运算:模运算是处理大数问题的有效方法,特别是在结果需要取模的情况下,熟练掌握模运算可以避免很多错误。
- 编写测试用例:在编写代码时,先思考测试用例,确保代码的正确性和鲁棒性。添加一些边界情况的测试用例,如卡牌数量为1、数组中包含0等。
- 代码优化:在确保代码正确性的基础上,可以考虑优化代码的性能,例如使用并行流处理大数组。