问题描述
小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
解题思路
本题目的关键在于通过动态规划统计满足条件的方案数。
1. 动态规划分析
-
状态定义:
设dp[i][j]表示前i张卡牌中,选择某些卡牌使得它们的数字和模 3 等于j的方案数。j的取值范围为[0, 2]。- 目标:计算
dp[n][0],即前n张卡牌数字之和模 3 等于 0 的方案数。
-
状态转移:
对于第i张卡牌,其正反面数字为a[i-1]和b[i-1]:-
如果选择正面,数字之和模 3 的更新公式为:
dp[i][(j+a[i−1])%3]+=dp[i−1][j]dp[i][(j + a[i-1]) % 3] += dp[i-1][j]dp[i][(j+a[i−1])%3]+=dp[i−1][j]
-
如果选择背面,数字之和模 3 的更新公式为:
dp[i][(j+b[i−1])%3]+=dp[i−1][j]dp[i][(j + b[i-1]) % 3] += dp[i-1][j]dp[i][(j+b[i−1])%3]+=dp[i−1][j]
计算时,需要对
10^9 + 7取模。 -
-
初始状态:
dp[0][0] = 1,表示前 0 张卡牌和为 0 的方案数为 1。- 其他状态
dp[0][1] = dp[0][2] = 0。
-
结果计算:
dp[n][0]即为答案。
2. 复杂度分析
-
时间复杂度:
- 外层循环遍历
n张卡牌:O(n)。 - 内层循环遍历 3 个余数状态:
O(3)。 - 总时间复杂度为
O(3n) = O(n)。
- 外层循环遍历
-
空间复杂度:
- 需要一个 n×3n \times 3n×3 的数组存储
dp状态,空间复杂度为O(3n)。 - 若优化为滚动数组,仅需
O(3)的空间。
- 需要一个 n×3n \times 3n×3 的数组存储
代码实现
核心代码:
java
代码解读
复制代码
public static int solution(int n, int[] a, int[] b)
{
final int MOD = 1000000007;
int[][] dp = new int[n + 1][3];
// 初始化
dp[0][0] = 1; // 前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 class Main { public static int solution(int n, int[] a, int[] b) { final int MOD = 1000000007; int[] dp = new int[3];
// 初始化:前0张卡牌,和为0的方案数为1
dp[0] = 1;
// 遍历每张卡牌
for (int i = 0; i < n; i++) {
int[] next = new int[3]; // 临时数组存储当前卡牌的状态转移
int front = a[i]; // 当前卡牌正面数字
int back = b[i]; // 当前卡牌背面数字
// 初始化 next 数组
for (int j = 0; j < 3; j++) {
next[j] = 0;
}
for (int j = 0; j < 3; j++) {
// 选择正面
next[(j + front) % 3] = (next[(j + front) % 3] + dp[j]) % MOD;
// 选择背面
next[(j + back) % 3] = (next[(j + back) % 3] + dp[j]) % MOD;
}
dp = next; // 更新 dp 为当前状态
}
// 返回前 n 张卡牌和模3余0的方案数
return dp[0];
}
public static void main(String[] args) {
System.out.println(solution(3, new int[]{1, 2, 3}, new int[]{2, 3, 2}) == 3); // 示例1
System.out.println(solution(4, new int[]{3, 1, 2, 4}, new int[]{1, 2, 3, 1}) == 6); // 示例2
System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 32); // 示例3
}
}
总结
利用豆包MarsCode AI不断进行优化
- 初始化
next数组:在每次更新next数组之前,将其所有元素初始化为0,以避免上一次循环的值影响当前循环。 - 使用临时变量:在更新
next数组时,直接将dp[j]加到next[(j + front) % 3]和next[(j + back) % 3]上,这样可以确保每次更新都是基于当前的dp数组。