这道题的核心任务是从给定的 n
张卡牌中,每张卡牌有两个面(正面和背面),选择其中一个面,使得所有选择的数字之和能够被 3 整除。初看起来,问题可能会让人感觉有些复杂,毕竟每张卡牌都有两个选择,而且数字和的组合数量很大。然而,通过动态规划的方式,我们可以把问题简化到只关注每个选择后的“余数”上,从而高效地解决它。
首先,理解题目中的关键要求至关重要:我们只关心数字和对 3 取余后的结果。无论数字本身有多大,最终我们关心的是它的余数。如果总和能被 3 整除,那么它的余数必定是 0。因此,问题的本质是:如何通过选择卡牌的正反面,使得所有卡牌数字之和的余数为 0。
如何理解余数
为了解决这个问题,我们引入了“余数”的概念。每次我们从卡牌中选择一个数字时,其实选择的是“这个数字与当前余数的关系”。具体来说,如果当前的数字和对 3 的余数是 rem
,那么选择卡牌的正面 a[i]
或背面 b[i]
,都会影响新的余数。通过计算 (rem + a[i]) % 3
或 (rem + b[i]) % 3
,我们可以推导出新的状态。
动态规划的设计
为了高效地解决这个问题,我们使用了动态规划(DP)的思想。我们定义了一个数组 dp
,其中:
dp[0]
表示当前选择的数字和对 3 取余为 0 的方案数。dp[1]
表示当前选择的数字和对 3 取余为 1 的方案数。dp[2]
表示当前选择的数字和对 3 取余为 2 的方案数。
最初,我们知道数字和为 0 的情况只有一种,那就是没有选择卡牌时。因此,dp[0] = 1
,而 dp[1]
和 dp[2]
都初始化为 0。
状态转移
对于每一张卡牌,它有两个选择:选择正面 a[i]
或背面 b[i]
。选择的每一个面都会影响余数的计算,因此我们需要根据当前的余数状态来更新 dp
数组。具体地,我们会依次检查每个可能的余数(0、1、2),然后根据选择的数字更新新的余数状态。为了避免在更新时覆盖掉上一步的计算结果,我们使用一个临时数组 newDp
来存储新的状态。
代码实现
public class Main {
public static int solution(int n, int[] a, int[] b) {
final int MOD = 1000000007;
// dp[0]:当前数字和 % 3 == 0 的方案数
// dp[1]:当前数字和 % 3 == 1 的方案数
// dp[2]:当前数字和 % 3 == 2 的方案数
long[] dp = new long[3];
dp[0] = 1; // 初始时,和为 0 的情况有 1 种(空集合)
for (int i = 0; i < n; i++) {
// 记录当前状态,避免更新时覆盖掉上次计算的值
long[] newDp = new long[3];
// 选择正面 a[i]
for (int rem = 0; rem < 3; rem++) {
newDp[(rem + a[i]) % 3] = (newDp[(rem + a[i]) % 3] + dp[rem]) % MOD;
}
// 选择背面 b[i]
for (int rem = 0; rem < 3; rem++) {
newDp[(rem + b[i]) % 3] = (newDp[(rem + b[i]) % 3] + dp[rem]) % MOD;
}
// 更新 dp
dp = newDp;
}
// 返回和 % 3 == 0 的方案数
return (int) dp[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);
}
}
时间和空间复杂度分析
-
时间复杂度:每张卡牌都有两种选择,每次选择会更新
dp
数组中的三个元素,因此每张卡牌的处理时间是O(3)
。最终的时间复杂度是O(n)
,其中n
是卡牌的数量。 -
空间复杂度:我们只需要一个长度为 3 的
dp
数组来存储余数状态,因此空间复杂度是O(1)
,也就是说,空间使用量不会随着输入规模的增加而变化。
总结
这道题的巧妙之处在于通过关注“余数”来简化问题,而动态规划则是解决这类组合问题的理想工具。通过逐步更新每个选择的余数状态,我们能够高效地计算出所有可能的方案,而不需要暴力枚举所有组合。通过这道题,我们不仅回顾了动态规划的核心思想,还学习了如何在特定问题中应用余数的特性,从而将问题的复杂度大大降低。
这种方法不仅限于本题,很多类似的组合优化问题都可以用余数来简化计算过程,掌握这个思维方式会让我们在面对其他问题时更加得心应手。希望这篇解题思路能对你有所启发,让你在算法的世界里游刃有余!