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

41 阅读5分钟

问题背景

小M有 n 张卡牌,每张卡牌的正面和背面各有一个不同的数字。我们的目标是选择每张卡牌的正面或背面,使得所有被选中卡牌的数字之和能被3整除。要求计算出所有可能的选择方案数,并返回结果对 109+710^9 + 7 取模后的值。

例如,对于 n = 3 和卡牌数据 a = [1, 2, 3]b = [2, 3, 2],我们需要计算所有的选择方式,使得数字和可以被3整除。通过合理的动态规划方法,我们可以高效地解决这个问题。

问题分析

该问题可以归结为一个典型的动态规划(DP)问题。我们需要计算卡牌正反面选择的组合数,使得所有选中的数字之和对3取模为0。由于数字可能较大,最终结果需要对 109+710^9 + 7 取模。

我们可以使用动态规划的思想,通过逐张卡牌的选择,更新当前和对3取模后的组合数。通过维护一个 dp 数组,表示当前数字和对3取模的不同状态,我们可以在每一步中决定是选择正面还是背面数字,从而推进到最终的解。

解决思路

  1. 状态转移

    • 用一个 dp 数组来记录当前所有组合的和对3的取模结果。dp[i] 表示和对3取模后为 i 的组合数。初始时,只有和为0的组合,即 dp[0] = 1
    • 对于每张卡牌,我们有两个选择:选择正面数字 a[i] 或背面数字 b[i]。每次选择都会更新和对3取模的结果。
  2. 动态规划数组的更新

    • 对于每张卡牌的选择,我们要将之前的 dp 数组中的每个状态转移到新的状态。具体地,如果当前状态 dp[j] 表示和对3取模为 j,则:

      • 选择正面时,新的状态为 (j + a[i]) % 3
      • 选择背面时,新的状态为 (j + b[i]) % 3
  3. 最终结果

    • 最终的答案是 dp[0],即和对3取模为0的所有组合数,因为我们要找的是能被3整除的组合。

动态规划实现

在动态规划中,我们需要处理每张卡牌的两种选择,并且注意每次更新时对数组的更新是基于前一个状态的,因此要小心避免“覆盖”问题。我们可以使用一个临时数组 new_dp 来存储每一轮的更新结果。

代码实现

def solution(n: int, a: list, b: list) -> int:
    MOD = 10**9 + 7  # 取模的常数
    
    # 初始化 dp 数组,dp[i] 表示当前和对 3 取模后的组合数
    dp = [0] * 3
    dp[0] = 1  # 初始时和为 0 的组合数为 1
    
    # 遍历每张卡牌
    for i in range(n):
        # 临时数组,用于存储更新后的 dp 值
        new_dp = [0] * 3
        
        # 对于当前卡牌的两个选择(正面或背面)
        for j in range(3):
            if dp[j] > 0:
                # 选择正面:将当前和加上 a[i],并对 3 取模
                new_dp[(j + a[i]) % 3] = (new_dp[(j + a[i]) % 3] + dp[j]) % MOD
                # 选择背面:将当前和加上 b[i],并对 3 取模
                new_dp[(j + b[i]) % 3] = (new_dp[(j + b[i]) % 3] + dp[j]) % MOD
        
        # 更新 dp 数组为新计算的 dp 值
        dp = new_dp
    
    # 最终结果是 dp[0],即和对 3 取模为 0 的组合数
    return dp[0]

if __name__ == '__main__':
    # 测试用例
    print(solution(n = 3, a = [1, 2, 3], b = [2, 3, 2]) == 3)  # 输出: 3
    print(solution(n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1]) == 6)  # 输出: 6
    print(solution(n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5]) == 32)  # 输出: 32

代码解析

  1. 初始化 DP 数组

    • dp[0] = 1 表示初始状态下,和为 0 的组合数是 1,因为没有选择任何卡牌时,和自然为 0。
    • dp[1]dp[2] 初始化为 0,表示没有任何和对 3 取模为 1 或 2 的组合。
  2. 遍历卡牌

    • 对每张卡牌的正反面进行两种选择。每当选择一个面时,就更新 new_dp 数组,表示新的组合状态。
    • new_dp[(j + a[i]) % 3] 表示选择正面后,更新当前和对 3 取模的结果。
    • 同理,new_dp[(j + b[i]) % 3] 表示选择背面后,更新当前和对 3 取模的结果。
  3. 更新 DP 数组

    • 每处理完一张卡牌后,更新 dp 数组为 new_dp,这样在下一轮中我们就可以继续使用新的组合状态。
  4. 返回结果

    • 最终,dp[0] 存储的是所有和对 3 取模为 0 的组合数,即最终能被 3 整除的方案数。

时间与空间复杂度

  • 时间复杂度:O(n),其中 n 是卡牌的数量。我们需要遍历 n 张卡牌,每次更新 dp 数组时只涉及常数时间的操作。
  • 空间复杂度:O(1),由于 dp 数组的大小为 3,且我们使用的 new_dp 数组也只占用常数空间,因此空间复杂度为常数级别。

测试用例

  1. 示例 1: 输入:n = 3, a = [1, 2, 3], b = [2, 3, 2] 输出:3

    解释:有3种选择方式使得和对3取模为0。

  2. 示例 2: 输入:n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1] 输出:6

    解释:有6种选择方式使得和对3取模为0。

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

    解释:有32种选择方式使得和对3取模为0。

总结

该问题通过动态规划的方式高效地计算了所有能够满足条件的选择方案数。利用 DP 数组记录当前和对3取模后的状态,避免了暴力枚举的时间复杂度。整体时间复杂度为 O(n),适合处理大规模的卡牌数量。同时,使用模 109+710^9 + 7 保证了结果不会溢出。