题解:卡牌选择问题 —— 让数字和能被 3 整除 | 豆包MarsCode AI刷题

3 阅读4分钟

这道题的核心任务是从给定的 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),也就是说,空间使用量不会随着输入规模的增加而变化。

总结

这道题的巧妙之处在于通过关注“余数”来简化问题,而动态规划则是解决这类组合问题的理想工具。通过逐步更新每个选择的余数状态,我们能够高效地计算出所有可能的方案,而不需要暴力枚举所有组合。通过这道题,我们不仅回顾了动态规划的核心思想,还学习了如何在特定问题中应用余数的特性,从而将问题的复杂度大大降低。

这种方法不仅限于本题,很多类似的组合优化问题都可以用余数来简化计算过程,掌握这个思维方式会让我们在面对其他问题时更加得心应手。希望这篇解题思路能对你有所启发,让你在算法的世界里游刃有余!