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

91 阅读4分钟

问题描述

小M有 n 张卡牌,每张卡牌的正反面分别写着不同的数字 a[i]b[i]。需要选择每张卡牌的一面,使得所有选择数字的总和可以被 3 整除。你的任务是统计满足条件的方案数,并将结果对 10^9 + 7 取模。

示例

输入:n = 3, a = [1, 2, 3], b = [2, 3, 2]
输出:3
解释:满足条件的方案有:

  • 选择 [1, 3, 2],和为 1 + 3 + 2 = 6
  • 选择 [2, 2, 2],和为 2 + 2 + 2 = 6
  • 选择 [2, 3, 3],和为 2 + 3 + 3 = 8

解题思路

本题的关键在于通过动态规划统计满足条件的方案数。

1. 动态规划分析

  • 状态定义
    dp[i][j] 表示前 i 张卡牌中,选择某些卡牌使得它们的数字和模 3 等于 j 的方案数。

    • j 的取值范围为 [0, 2]
    • 目标:计算 dp[n][0],即前 n 张卡牌数字之和模 3 等于 0 的方案数。
  • 状态转移
    对于第 i 张卡牌,其正反面数字为 a[i-1]b[i-1]

    • 如果选择正面,数字之和模 3 的更新公式为:
      dp[i][(j+a[i1])%3]+=dp[i1][j]dp[i][(j + a[i-1]) \% 3] += dp[i-1][j]
    • 如果选择背面,数字之和模 3 的更新公式为:
      dp[i][(j+b[i1])%3]+=dp[i1][j]dp[i][(j + b[i-1]) \% 3] += dp[i-1][j]

    计算时,需要对 10^9 + 7 取模。

  • 初始状态

    • dp[0][0] = 1,表示前 0 张卡牌和为 0 的方案数为 1。
    • 其他状态 dp[0][1] = dp[0][2] = 0
  • 结果计算

    • dp[n][0] 即为答案。

2. 复杂度分析

  • 时间复杂度

    • 外层循环遍历 n 张卡牌:O(n)
    • 内层循环遍历 3 个余数状态:O(3)
    • 总时间复杂度为 O(3n) = O(n)
  • 空间复杂度

    • 需要一个 n×3n \times 3 的数组存储 dp 状态,空间复杂度为 O(3n)
    • 若优化为滚动数组,仅需 O(3) 的空间。

代码实现

核心代码:

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; // 前0张卡牌和为0的方案数为1

    // 动态规划
    for (int i = 1; i <= n; i++)
    {
        int front = a[i - 1]; // 当前卡牌正面数字
        int back = b[i - 1];  // 当前卡牌背面数字
        for (int j = 0; j < 3; j++)
        {
            // 转移方程:选择正面或背面
            dp[i][(j + front) % 3] = (dp[i][(j + front) % 3] + dp[i - 1][j]) % MOD;
            dp[i][(j + back) % 3] = (dp[i][(j + back) % 3] + dp[i - 1][j]) % MOD;
        }
    }

    return dp[n][0];
}

示例分析

示例 1

输入:n = 3, a = [1, 2, 3], b = [2, 3, 2]

  1. 初始化dp[0][0] = 1, dp[0][1] = dp[0][2] = 0
  2. 第 1 张卡牌
    • 正面 1,背面 2。更新后:
      • dp[1][1] = dp[0][0] = 1
      • dp[1][2] = dp[0][0] = 1
  3. 第 2 张卡牌
    • 正面 2,背面 3。更新后:
      • dp[2][0] = dp[1][1] + dp[1][2] = 2
      • 其他状态类似。
  4. 第 3 张卡牌
    • 正面 3,背面 2。最终 dp[3][0] = 3

输出:3


代码优化

  1. 滚动数组
    当前状态只与上一状态有关,可以用滚动数组优化空间复杂度:

    int[] dp = new int[3];
    dp[0] = 1;
    for (int i = 0; i < n; i++)
    {
        int[] next = new int[3];
        for (int j = 0; j < 3; j++)
        {
            next[(j + a[i]) % 3] = (next[(j + a[i]) % 3] + dp[j]) % MOD;
            next[(j + b[i]) % 3] = (next[(j + b[i]) % 3] + dp[j]) % MOD;
        }
        dp = next;
    }
    
  2. 完整代码

    public class Main
    {
        public static int solution(int n, int[] a, int[] b)
        {
            final int MOD = 1000000007;
            int[] dp = new int[3];
    
            // 初始化:前0张卡牌,和为0的方案数为1
            dp[0] = 1;
    
            // 遍历每张卡牌
            for (int i = 0; i < n; i++)
            {
                int[] next = new int[3]; // 临时数组存储当前卡牌的状态转移
                int front = a[i];       // 当前卡牌正面数字
                int back = b[i];        // 当前卡牌背面数字
    
                for (int j = 0; j < 3; j++)
                {
                    // 选择正面
                    next[(j + front) % 3] = (next[(j + front) % 3] + dp[j]) % MOD;
                    // 选择背面
                    next[(j + back) % 3] = (next[(j + back) % 3] + dp[j]) % MOD;
                }
    
                dp = next; // 更新 dp 为当前状态
            }
    
            // 返回前 n 张卡牌和模3余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); // 示例1
            System.out.println(solution(4, new int[]{3, 1, 2, 4}, new int[]{1, 2, 3, 1}) == 6); // 示例2
            System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 32); // 示例3
        }
    }
    

总结

  • 本题巧妙利用动态规划解决模 3 问题,核心在于合理设计状态转移方程。
  • 滚动数组优化进一步减少了空间复杂度。
  • 动态规划思想适合处理类似的“分组求和”问题,具备广泛的应用场景。