33 卡牌翻面求和 | 豆包MarsCode AI刷题

48 阅读5分钟

卡牌翻面求和:一个动态规划问题的详细解析

问题描述

给定 n 张卡牌,每张卡牌有正反两面,正面写着数字 a[i],反面写着数字 b[i]。我们需要通过选择每张卡牌的正面或反面,使得所有卡牌数字之和可以被3整除。问题的目标是:计算出所有满足条件的方案总数。

输入与输出:

  • 输入:
    • 整数 n 表示卡牌的数量。
    • 数组 a[]b[],分别表示每张卡牌的正面与背面上的数字。
  • 输出:
    • 输出满足条件的方案数,结果需要对 10^9 + 7 取模。

示例:

样例 1

输入:

n = 3
a = [1, 2, 3]
b = [2, 3, 2]

输出:

3

解释: 在这个例子中,卡牌的选择方案包括:

  1. 选择卡牌的正面(1, 2, 2);
  2. 选择卡牌的正面(1, 3, 2);
  3. 选择卡牌的正面(1, 2, 2)。

满足条件的方案数量为3。


思路解析

1. 问题归类:动态规划

从题目中可以看出,卡牌的选择并不独立,而是相互影响的。每张卡牌的选择都会影响最终的结果,因此这类问题本质上可以用 动态规划 来求解。我们要解决的问题是:如何将每张卡牌的正面或背面的选择转化为一个动态规划问题,并最终得到满足条件的方案数。

2. 状态定义

为了使用动态规划,我们需要定义一个状态来表示当前的选择情况。我们不关心每一张卡牌的具体选择,而是关心当前所有选择的和对3取余的结果。我们可以定义一个状态数组 dp,其中:

  • dp[j] 表示当前已经选择的卡牌和对3取余结果为 j 的方案数。j 的值可以是 0, 1 或 2(因为我们关心的是对3的余数)。

3. 初始状态

初始时,我们还没有选择任何卡牌,因此和对3取余的结果自然是0,即 dp[0] = 1。也就是说,刚开始没有选择任何卡牌时,我们的方案数只有1种。

4. 状态转移

每一张卡牌 i 都有两个选择:

  • 选择卡牌的正面 a[i]
  • 选择卡牌的反面 b[i]

我们要做的,就是更新 dp 数组。对于每一张卡牌的选择,都要基于当前的 dp 数组来更新。具体来说,如果我们已经得到了一个和对3取余为 j 的方案,那么:

  • 选择卡牌的正面后,新的和对3取余是 (j + a[i]) % 3
  • 选择卡牌的反面后,新的和对3取余是 (j + b[i]) % 3

这意味着,每个 dp[j] 的值都会影响到后续的选择,因此我们需要用一个新的数组 newDp 来存储新的状态。

5. 最终结果

最终,我们要得到的是所有卡牌的选择中,和对3取余为0的方案数。也就是 dp[0]


动态规划实现

public class Main {
    public static int solution(int n, int[] a, int[] b) {
        final int MOD = 1000000007;

        // dp[i]表示前i张卡牌中,和对3取余为i的方案数
        int[] dp = new int[3];
        dp[0] = 1; // 初始状态:没有卡牌时,和被3整除的方案数为1

        for (int i = 0; i < n; i++) {
            // 记录当前卡牌的选择影响
            int[] newDp = new int[3];

            for (int j = 0; j < 3; j++) {
                // 如果选择正面 a[i]
                newDp[(j + a[i]) % 3] = (newDp[(j + a[i]) % 3] + dp[j]) % MOD;
                // 如果选择背面 b[i]
                newDp[(j + b[i]) % 3] = (newDp[(j + b[i]) % 3] + dp[j]) % MOD;
            }

            // 更新dp数组
            dp = newDp;
        }

        // 返回和为3的倍数的方案数,即 dp[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);
        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);
    }
}

代码详解:

  1. dp[0] = 1:表示没有卡牌时,和为0的方案数为1。
  2. newDp[(j + a[i]) % 3]newDp[(j + b[i]) % 3]:这两行代码更新了新的状态,其中 a[i]b[i] 分别表示当前卡牌的正面和反面数字,通过取模运算来确保结果符合对3取余的要求。
  3. dp = newDp:在每一轮更新后,我们将 newDp 的值赋给 dp,以便进入下一张卡牌的选择。

时间与空间复杂度:

  • 时间复杂度:每次循环中,我们更新 dp 数组的三个状态,因此时间复杂度是 O(n),其中 n 是卡牌的数量。
  • 空间复杂度:我们使用了一个固定大小为3的数组 dp,所以空间复杂度是 O(1)

思考与优化

  1. 状态压缩:在此问题中,我们只关心和对3的余数,因此 dp 数组的大小仅为3,可以节省空间。通过这种方式,我们在处理类似的问题时,可以降低空间复杂度。
  2. 模运算的重要性:由于最终的结果需要对 10^9 + 7 取模,我们在每一步的状态转移中都使用了模运算,确保数值不会溢出。这在处理大数问题时非常重要。
  3. 动态规划的核心思想:此问题实际上是经典的背包问题的变种,我们的背包容量是3(即我们关心的状态是0, 1, 2)。通过动态规划,我们能够高效地求解出符合条件的方案数。

总结

通过这道卡牌翻面求和问题,我们深入探讨了动态规划的核心思想及其应用。通过定义状态、状态转移和最优解的计算,我们能够在合理的时间复杂度内解决问题。在实际编程过程中,动态规划的思想可以帮助我们高效解决许多复杂的优化问题。希望这篇文章能帮助大家更好地理解和掌握动态规划的基本技巧。