伴学笔记1

91 阅读4分钟

问题描述

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

例如:如果有3张卡牌,正反面数字分别为 (1,2)(2,3) 和 (3,2),你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。

测试样例

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

样例3:

输入:n = 5 ,a = [1, 2, 3, 4, 5] ,b = [1, 2, 3, 4, 5]
输出:32

解题思路

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

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[i−1])%3]+=dp[i−1][j]dp[i][(j + a[i-1]) % 3] += dp[i-1][j]dp[i][(j+a[i−1])%3]+=dp[i−1][j]

    • 如果选择背面,数字之和模 3 的更新公式为:

      dp[i][(j+b[i−1])%3]+=dp[i−1][j]dp[i][(j + b[i-1]) % 3] += dp[i-1][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 3n×3 的数组存储 dp 状态,空间复杂度为 O(3n)
    • 若优化为滚动数组,仅需 O(3) 的空间。

代码实现

核心代码:

java
 代码解读
复制代码
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. 完整代码

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];        // 当前卡牌背面数字

        // 初始化 next 数组
        for (int j = 0; j < 3; j++) {
            next[j] = 0;
        }

        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
}

}

总结

利用豆包MarsCode AI不断进行优化

  1. 初始化 next 数组:在每次更新 next 数组之前,将其所有元素初始化为0,以避免上一次循环的值影响当前循环。
  2. 使用临时变量:在更新 next 数组时,直接将 dp[j] 加到 next[(j + front) % 3] 和 next[(j + back) % 3] 上,这样可以确保每次更新都是基于当前的 dp 数组。