困难题:卡牌翻面求和问题 | 豆包MarsCode AI刷题

81 阅读5分钟

题目解析

今天我来分享一道我在刷题时遇到的有趣的动态规划题——卡牌翻面求和问题。题目要求我们通过选择每张卡牌的正面或背面,使得所有卡牌上数字之和能被3整除,求出符合条件的方案数。这是一个典型的动态规划问题,涉及到求解余数的问题,我在解题过程中总结了一些关键点,今天与大家分享一下我的解题过程。

题目描述

小M拥有 n 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 a_i,背面是 b_i。小M希望通过选择每张卡牌的一面,使得所有卡牌上数字之和能够被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 10^9 + 7 取模。

输入与输出

输入

  • n:卡牌的数量
  • a:卡牌正面数字数组,长度为 n
  • b:卡牌背面数字数组,长度为 n

输出

  • 满足条件的不同选择方案数,结果对 10^9 + 7 取模。

示例

示例 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

关键思路

这个题目让我第一次深刻理解了动态规划在处理带有余数和选择限制的问题时的应用。

1. 余数的状态转移

我将问题转化成了动态规划的问题,定义一个状态 dp[rem],表示当前选择的卡牌上数字和除以 3 后的余数为 rem 的方案数。rem 可以是 0, 1, 或 2,因为任何整数除以 3 的余数只有这三种可能。

初始化

  • dp[0] = 1:表示没有选择卡牌时,数字和为 0,余数自然是 0,方案数为 1。
  • dp[1] = dp[2] = 0:表示初始时,余数为 1 或 2 的方案数为 0。

2. 状态转移

对于每张卡牌,我们有两个选择:选择正面数字 a[i] 或选择背面数字 b[i]。我根据这两种选择计算当前余数的变化:

  • 如果我选择卡牌的正面(即选择 a[i]),那么新的余数为 (rem + a[i] % 3) % 3
  • 如果我选择卡牌的背面(即选择 b[i]),那么新的余数为 (rem + b[i] % 3) % 3

这样,我就可以通过遍历每张卡牌,依次更新 dp 数组,最后得出所有方案数。

3. 终极目标

最终的目标是求解所有选择卡牌的数字和能够被 3 整除的方案数,即 dp[0]。因为我们希望数字和能被 3 整除,所以只关心 rem = 0 的情况。

4. 动态规划代码实现

下面是我根据上述思路写的代码实现:

public class Main {
    public static int solution(int n, int[] a, int[] b) {
        final int MOD = 1_000_000_007;
        
        // dp[rem] 表示余数为 rem 的方案数
        long[] dp = new long[3];
        dp[0] = 1; // 初始状态:数字和为 0 时,余数为 0 的方案数是 1
        
        // 遍历每张卡牌
        for (int i = 0; i < n; i++) {
            int numA = a[i] % 3; // a[i] 对 3 取余
            int numB = b[i] % 3; // b[i] 对 3 取余
            
            // 为了避免修改 dp 数组中的当前值,先使用一个临时数组保存新的结果
            long[] newDp = new long[3];
            
            for (int rem = 0; rem < 3; rem++) {
                // 如果当前余数为 rem,选择 a[i] 或 b[i] 都可能改变余数
                newDp[(rem + numA) % 3] = (newDp[(rem + numA) % 3] + dp[rem]) % MOD;
                newDp[(rem + numB) % 3] = (newDp[(rem + numB) % 3] + dp[rem]) % MOD;
            }
            
            // 更新 dp 数组为新的 dp 数组
            dp = newDp;
        }
        
        // 最终我们需要 dp[0],即数字和能被 3 整除的方案数
        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);
    }
}

代码解释

在这段代码中,我定义了一个长度为 3 的 dp 数组,用来记录不同余数(0, 1, 2)的方案数。接着,我遍历每张卡牌,根据卡牌正面或背面选择带来的余数变化,更新 dp 数组。为了避免在遍历过程中修改正在计算的 dp 数组,我使用了一个临时的 newDp 数组来保存当前状态的更新。

最后,我返回 dp[0],即满足数字和能被 3 整除的所有方案数。

知识总结

通过这道题目,我总结了以下几个关键的知识点:

  1. 动态规划在求余数问题中的应用: 动态规划可以有效地解决带有余数限制的选择问题。通过定义状态来追踪当前余数,从而求解符合条件的方案数。

  2. 空间优化: 在更新 dp 数组时,我通过使用临时数组 newDp 避免了在同一轮计算中修改正在计算的数组,这避免了潜在的错误。

  3. 取模操作: 由于方案数可能非常大,所以我们在每次更新 dp 数组时,都使用了 MOD = 1_000_000_007 来取模,确保结果不溢出并符合题目要求。

通过这道题目,我不仅加深了对动态规划的理解,还学习了如何处理带余数的选择问题,并在实际应用中有效地控制状态的转移和优化。