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

39 阅读3分钟

一、问题描述

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


这是一道动态规划和模运算相结合的典型问题,考察了对状态转移的设计能力和对约束条件的处理技巧。

二、解题思路

1. 关键分析

  • 模3的性质:对于一个数字 xx,它对3取模的结果只有三种可能:001122。我们希望最终选择的数字总和模3等于0。
  • 动态规划
    • 状态定义:dp[j]dp[j] 表示当前卡牌选择后,模3等于 jj 的方案数。
    • 状态转移:当处理第 ii 张卡牌时,我们有两种选择:选择正面或反面。两种选择分别更新对应模值下的方案数。
  • 初始状态:如果没有卡牌,只有一种方案,即总和为0(模3等于0),因此 dp[0]=1dp[0] = 1,其余状态为0。

2. 动态规划转移方程

设第 ii 张卡牌的正反面分别为 aia_ibib_i,它们对3的余数分别为 ra=ai%3r_a = a_i \% 3rb=bi%3r_b = b_i \% 3。更新 dpdp 数组时:

new_dp[(j+ra)%3]+=dp[j]new\_dp[(j + r_a) \% 3] += dp[j]

new_dp[(j+rb)%3]+=dp[j]new\_dp[(j + r_b) \% 3] += dp[j]

我们用一个新数组来记录当前卡牌的选择对状态的更新,避免直接修改 dpdp 影响后续计算。


三、滚动数组优化

在原始方法中,dp[i][j]dp[i][j] 记录前 ii 张卡牌的状态,每次计算第 i+1i+1 张卡牌时需要使用前一层的所有状态。然而,观察状态转移方程发现,更新仅依赖于前一状态。我们可以通过 滚动数组 优化,将二维数组 dp[i][j]dp[i][j] 压缩为一维数组 dp[j]dp[j]

具体做法如下:

  1. 在每次处理一张卡牌时,临时记录新的状态 new_dpnew\_dp
  2. 更新完当前卡牌对应的 new_dpnew\_dp 后,将 new_dpnew\_dp 替换为 dpdp
  3. 每次只需维护一个固定大小为3的数组,从而将空间复杂度优化为 O(1)O(1)

四、代码实现

下面是用 Python 实现的滚动数组优化代码:

mod = 10**9 + 7

def solution(n: int, a: list, b: list) -> int:
    dp = [1, 0, 0]
    for r_a, r_b in zip(a, b):
        new_dp = [0] * 3
        for j in range(3):
            new_dp[(j + r_a) % 3] = (new_dp[(j + r_a) % 3] + dp[j]) % mod
            new_dp[(j + r_b) % 3] = (new_dp[(j + r_b) % 3] + dp[j]) % mod
        dp = new_dp
    return dp[0]

五、复杂度分析

1. 时间复杂度

  • 遍历卡牌:对于每张卡牌,我们需要进行一次状态更新,总共有 nn 张卡牌,因此遍历卡牌的时间复杂度是 O(n)O(n)
  • 更新状态:每张卡牌的状态转移中,涉及三个模值的更新,即 j=0,1,2j = 0, 1, 2,每次更新是常数时间 O(1)O(1)。因此,单张卡牌的状态更新时间复杂度为 O(1)O(1)
  • 总体时间复杂度:综合起来,总体时间复杂度为 O(n×1)=O(n)O(n \times 1) = O(n)

2. 空间复杂度

  • 在使用滚动数组优化后,我们仅需维护一个长度为3的数组 dpdp 来记录当前状态。每次更新时临时使用一个同样大小的 new_dpnew\_dp 数组,因此空间复杂度为 O(3)=O(1)O(3) = O(1),我们未引入其他复杂的数据结构,因此空间复杂度保持在 O(1)O(1)