问题描述
小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 整除,因此模 3 是问题的核心。
解题思路
状态定义
将问题转化为动态规划的形式后,定义 dp[i][r] 表示前 i 张卡牌中,选择某一面的所有可能组合,使得数字之和模 3 的余数为 r 的方案数。这里 r 的取值范围为 {0, 1, 2}。
状态转移方程
对于每张卡牌,可以选择正面或背面,将两种选择的结果加入动态规划表中:
- 如果选择第
i张卡牌的正面,当前余数r转移为(r + a[i]) % 3。 - 如果选择第
i张卡牌的背面,当前余数r转移为(r + b[i]) % 3。
状态转移公式如下:
dp[i][(r + a[i]) % 3] += dp[i-1][r]
dp[i][(r + b[i]) % 3] += dp[i-1][r]
初始化与结果获取
在初始化阶段,只有第一张卡牌时,可以直接选择正面或背面:
dp[1][a[1] % 3] = 1
dp[1][b[1] % 3] = 1
最终答案为 dp[n][0],即前 n 张卡牌中,和模 3 为 0 的方案数。
代码实现
以下是完整的代码实现(已包含注释):
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;
for (int i = 1; i <= n; i++) {
int aMod = a[i - 1] % 3;
int bMod = b[i - 1] % 3;
for (int j = 0; j < 3; j++) {
dp[i][(j + aMod) % 3] = (dp[i][(j + aMod) % 3] + dp[i - 1][j]) % MOD;
dp[i][(j + bMod) % 3] = (dp[i][(j + bMod) % 3] + dp[i - 1][j]) % MOD;
}
}
return dp[n][0];
}
个人思考与分析
模块化思想的力量
在处理复杂问题时,模运算可以将状态空间压缩到一个有限范围。本问题中,通过对 3 取模,将所有可能的和归约到 {0, 1, 2} 三种状态,大大降低了问题的复杂性。这种方法不仅节省了时间,还降低了内存消耗。
空间优化的潜力
虽然上述实现使用了二维数组记录所有状态,但可以进一步优化为一维数组以节约空间。通过在状态转移时使用临时数组,我们可以只保留当前与前一状态,从而将空间复杂度从 O(n×3) 降至 O(3)。优化后的代码如下:
int[] dp = new int[3];
dp[0] = 1;
for (int i = 0; i < n; i++) {
int aMod = a[i] % 3, bMod = b[i] % 3;
int[] next = new int[3];
for (int j = 0; j < 3; j++) {
next[(j + aMod) % 3] = (next[(j + aMod) % 3] + dp[j]) % MOD;
next[(j + bMod) % 3] = (next[(j + bMod) % 3] + dp[j]) % MOD;
}
dp = next;
}
return dp[0];
模运算在现实中的应用
模运算不仅仅在算法竞赛中出现,在实际应用中也广泛存在。例如:
- 加密算法:如 RSA 算法中,大数计算需要模运算进行结果限制。
- 资源调度:在处理循环任务时,用模运算确定下一个任务的位置。