问题描述
小M有 n 张卡牌,每张卡牌的正反面分别写着不同的数字 a[i] 和 b[i]。需要选择每张卡牌的一面,使得所有选择数字的总和可以被 3 整除。你的任务是统计满足条件的方案数,并将结果对 10^9 + 7 取模。
示例
输入:n = 3, a = [1, 2, 3], b = [2, 3, 2]
输出:3
解释:满足条件的方案有:
- 选择
[1, 3, 2],和为1 + 3 + 2 = 6。 - 选择
[2, 2, 2],和为2 + 2 + 2 = 6。 - 选择
[2, 3, 3],和为2 + 3 + 3 = 8。
解题思路
本题的关键在于通过动态规划统计满足条件的方案数。
1. 动态规划分析
-
状态定义:
设dp[i][j]表示前i张卡牌中,选择某些卡牌使得它们的数字和模 3 等于j的方案数。j的取值范围为[0, 2]。- 目标:计算
dp[n][0],即前n张卡牌数字之和模 3 等于 0 的方案数。
-
状态转移:
对于第i张卡牌,其正反面数字为a[i-1]和b[i-1]:- 如果选择正面,数字之和模 3 的更新公式为:
- 如果选择背面,数字之和模 3 的更新公式为:
计算时,需要对
10^9 + 7取模。 - 如果选择正面,数字之和模 3 的更新公式为:
-
初始状态:
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)。
- 外层循环遍历
-
空间复杂度:
- 需要一个 的数组存储
dp状态,空间复杂度为O(3n)。 - 若优化为滚动数组,仅需
O(3)的空间。
- 需要一个 的数组存储
代码实现
核心代码:
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];
}
示例分析
示例 1
输入:n = 3, a = [1, 2, 3], b = [2, 3, 2]
- 初始化:
dp[0][0] = 1, dp[0][1] = dp[0][2] = 0 - 第 1 张卡牌:
- 正面
1,背面2。更新后:dp[1][1] = dp[0][0] = 1dp[1][2] = dp[0][0] = 1
- 正面
- 第 2 张卡牌:
- 正面
2,背面3。更新后:dp[2][0] = dp[1][1] + dp[1][2] = 2- 其他状态类似。
- 正面
- 第 3 张卡牌:
- 正面
3,背面2。最终dp[3][0] = 3。
- 正面
输出:3。
代码优化
-
滚动数组:
当前状态只与上一状态有关,可以用滚动数组优化空间复杂度:int[] dp = new int[3]; dp[0] = 1; for (int i = 0; i < n; i++) { int[] next = new int[3]; for (int j = 0; j < 3; j++) { next[(j + a[i]) % 3] = (next[(j + a[i]) % 3] + dp[j]) % MOD; next[(j + b[i]) % 3] = (next[(j + b[i]) % 3] + dp[j]) % MOD; } dp = next; } -
完整代码:
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]; // 当前卡牌背面数字 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 } }
总结
- 本题巧妙利用动态规划解决模 3 问题,核心在于合理设计状态转移方程。
- 滚动数组优化进一步减少了空间复杂度。
- 动态规划思想适合处理类似的“分组求和”问题,具备广泛的应用场景。