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

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

思路

这道题可以视为对 0/1 背包问题的变形,不过0/1 背包问题通常是求最大价值,而卡牌翻面问题是求所有组合数。为了解决这个问题,我们可以使用动态规划来跟踪不同选择下的状态。

1. 状态定义

我们定义一个二维数组 dp,其中 dp[i][j] 表示前 i 张卡牌中,选择的数字和模3为 j 的组合数。这里 j 的取值为 0、1、2,分别表示当前和对3的余数。

2. 初始化

dp[0][0] = 1,表示不选择任何卡牌时,数字和为 0,这显然满足模3为0的条件。

3. 状态转移

对于每张卡牌,我们有两个选择:

  • 选择正面 a[i−1]a[i-1]a[i−1]:则新的模3值为 (j + a[i-1]) % 3
  • 选择背面 b[i−1]b[i-1]b[i−1]:则新的模3值为 (j + b[i-1]) % 3

在计算过程中,我们需要遍历当前状态 j 的所有可能情况,并根据选择的面更新 dp 状态。这样,在每次选择卡牌后,我们都会形成新的状态,并将结果累加到 dp 数组中。

4. 结果计算

最终,我们需要的结果就是所有选择后和模3为0的组合数,显然对应的就是 dp[n][0]

示例演示

例如:假设我们有三张卡牌,卡牌的数字如下:

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

其选择过程如下所示:

步骤1 初始化

dp[0] = [1, 0, 0]  // 不选择任何卡牌,和为 0

步骤 2:选择第一张卡牌

选择正面 1dp[1][(0 + 1) % 3] += dp[0][0] => dp[1][1] += 1
选择背面 2dp[1][(0 + 2) % 3] += dp[0][0] => dp[1][2] += 1

现在 dp[1] = [1, 1, 1]  // 和为 0 或 1 或 2

步骤 3:选择第二张卡牌

选择正面 2:
dp[2][(0 + 2) % 3] += dp[1][0] => dp[2][2] += 1
dp[2][(1 + 2) % 3] += dp[1][1] => dp[2][0] += 1
dp[2][(2 + 2) % 3] += dp[1][2] => dp[2][1] += 1

选择背面 3:
dp[2][(0 + 3) % 3] += dp[1][0] => dp[2][0] += 1
dp[2][(1 + 3) % 3] += dp[1][1] => dp[2][1] += 1
dp[2][(2 + 3) % 3] += dp[1][2] => dp[2][2] += 1

现在 dp[2] = [2, 2, 2]

步骤 4:选择第三张卡牌

以此类推,继续更新状态直到处理完所有卡牌。

最终,得到 dp[n][0],它的值就是满足条件的方案数。

以下是Python代码实现:

def solution(n: int, a: list, b: list) -> int:
    MOD = 10**9 + 7
    
    # dp[i][j]表示前i张卡牌,选择的数字和模3为j的组合数
    dp = [[0] * 3 for _ in range(n + 1)]
    
    # 初始化
    dp[0][0] = 1  # 不选择任何卡牌,和为0
    
    for i in range(1, n + 1):
        for j in range(3):
            # 如果选择正面
            dp[i][(j + a[i - 1]) % 3] = (dp[i][(j + a[i - 1]) % 3] + dp[i - 1][j]) % MOD
            # 如果选择背面
            dp[i][(j + b[i - 1]) % 3] = (dp[i][(j + b[i - 1]) % 3] + dp[i - 1][j]) % MOD
    
    # 结果是所有选择的和模3为0的方案数
    return dp[n][0]

if __name__ == '__main__':
    print(solution(n = 3, a = [1, 2, 3], b = [2, 3, 2]) == 3)
    print(solution(n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1]) == 6)
    print(solution(n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5]) == 32)

总结

解决此问题的关键在于利用动态规划的思想来系统地追踪和的模 3 状态。核心在于将问题分解为较小的子问题,通过构建状态转移方程,将每张卡牌的选择与当前状态关联起来,从而逐步计算出所有可能的组合。

首先,明确状态定义非常重要。我们使用 dp[i][j] 来表示前 i 张卡牌中选择的数字和模 3 等于 j 的组合数。这一结构化的表示使得我们可以清晰地看到每个状态之间的关系。其次,状态转移方程的构造是动态规划的关键。我们通过选择每张卡牌的正面或背面,计算出每种选择对当前模 3 状态的影响,确保所有可能的组合都被考虑到。