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

99 阅读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


基础知识点解析

模运算与问题特性

模运算在计算中非常重要。尤其在求解能整除某数的问题时,模运算可以帮助我们高效地确定当前数值和目标之间的关系。本问题中特别要求数字之和被 3 整除,因此模 3 是问题的核心。


解题思路

状态定义

将问题转化为动态规划的形式后,定义 dp[i][r] 表示前 i 张卡牌中,选择某一面的所有可能组合,使得数字之和模 3 的余数为 r 的方案数。这里 r 的取值范围为 {0, 1, 2}

状态转移方程

对于每张卡牌,可以选择正面或背面,将两种选择的结果加入动态规划表中:

  1. 如果选择第 i 张卡牌的正面,当前余数 r 转移为 (r + a[i]) % 3
  2. 如果选择第 i 张卡牌的背面,当前余数 r 转移为 (r + b[i]) % 3

状态转移公式如下:

dp[i][(r + a[i]) % 3] += dp[i-1][r]
dp[i][(r + b[i]) % 3] += dp[i-1][r]

初始化与结果获取

在初始化阶段,只有第一张卡牌时,可以直接选择正面或背面:

dp[1][a[1] % 3] = 1
dp[1][b[1] % 3] = 1

最终答案为 dp[n][0],即前 n 张卡牌中,和模 3 为 0 的方案数。


代码实现

以下是完整的代码实现(已包含注释):

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;

    for (int i = 1; i <= n; i++) {
        int aMod = a[i - 1] % 3;
        int bMod = b[i - 1] % 3;

        for (int j = 0; j < 3; j++) {
            dp[i][(j + aMod) % 3] = (dp[i][(j + aMod) % 3] + dp[i - 1][j]) % MOD;
            dp[i][(j + bMod) % 3] = (dp[i][(j + bMod) % 3] + dp[i - 1][j]) % MOD;
        }
    }

    return dp[n][0];
}

个人思考与分析

模块化思想的力量

在处理复杂问题时,模运算可以将状态空间压缩到一个有限范围。本问题中,通过对 3 取模,将所有可能的和归约到 {0, 1, 2} 三种状态,大大降低了问题的复杂性。这种方法不仅节省了时间,还降低了内存消耗。

空间优化的潜力

虽然上述实现使用了二维数组记录所有状态,但可以进一步优化为一维数组以节约空间。通过在状态转移时使用临时数组,我们可以只保留当前与前一状态,从而将空间复杂度从 O(n×3) 降至 O(3)。优化后的代码如下:

int[] dp = new int[3];
dp[0] = 1;

for (int i = 0; i < n; i++) {
    int aMod = a[i] % 3, bMod = b[i] % 3;
    int[] next = new int[3];

    for (int j = 0; j < 3; j++) {
        next[(j + aMod) % 3] = (next[(j + aMod) % 3] + dp[j]) % MOD;
        next[(j + bMod) % 3] = (next[(j + bMod) % 3] + dp[j]) % MOD;
    }

    dp = next;
}

return dp[0];

模运算在现实中的应用

模运算不仅仅在算法竞赛中出现,在实际应用中也广泛存在。例如:

  • 加密算法:如 RSA 算法中,大数计算需要模运算进行结果限制。
  • 资源调度:在处理循环任务时,用模运算确定下一个任务的位置。