困难题:卡牌翻面求和问题

47 阅读5分钟

卡牌翻面求和问题

一、问题描述

小M拥有 ( n ) 张卡牌,每张卡牌的正反面分别写着不同的数字,正面为 ( a_i ),背面为 ( b_i )。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被 3 整除。我们的任务是计算出所有满足该条件的方案数量,并对 ( 10^9 + 7 ) 取模。

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

二、思路解析

这个问题可以通过动态规划来解决。我们需要记录在选择前 ( i ) 张卡牌的情况下,所有可能的和对 3 取模的方案数。动态规划的状态转移可以通过以下步骤进行:

  1. 状态定义

    • 设 ( dp[i][j] ) 为前 ( i ) 张卡牌中,和对 3 取模为 ( j ) 的方案数,其中 ( j ) 的取值为 0, 1, 2(分别表示和对 3 取模的结果)。
  2. 状态转移

    • 对于每张卡牌 ( i ),我们可以选择正面 ( a[i-1] ) 或背面 ( b[i-1] )。
    • 如果选择正面 ( a[i-1] ),则和对 3 取模的值会变为 ( (j + a[i-1]) \mod 3 )。
    • 如果选择背面 ( b[i-1] ),则和对 3 取模的值会变为 ( (j + b[i-1]) \mod 3 )。
    • 因此,状态转移方程可以表示为:
      • ( dp[i][(j + a[i-1]) \mod 3] += dp[i-1][j] )
      • ( dp[i][(j + b[i-1]) \mod 3] += dp[i-1][j] )
  3. 初始状态

    • ( dp[0][0] = 1 ):表示没有卡牌时,和为 0 的方案数为 1。
  4. 计算结果

    • 最终结果为 ( dp[n][0] ),即前 ( n ) 张卡牌中,和对 3 取模为 0 的方案数量。

三、步骤

  1. 初始化动态规划数组 ( dp )。
  2. 设置初始状态 ( dp[0][0] = 1 )。
  3. 遍历每张卡牌,更新 ( dp ) 数组。
  4. 返回 ( dp[n][0] ) 的值。

四、图解

假设我们有 3 张卡牌,正反面数字为:

  • 卡牌 1: ( a_1 = 1, b_1 = 2 )
  • 卡牌 2: ( a_2 = 2, b_2 = 3 )
  • 卡牌 3: ( a_3 = 3, b_3 = 2 )

在动态规划过程中,状态转移如下:

  • 初始状态 ( dp[0] = [1, 0, 0] )
  • 选择卡牌 1:
    • 选择正面 ( a_1 ):( dp[1][1] += dp[0][0] ) → ( dp[1] = [0, 1, 0] )
    • 选择背面 ( b_1 ):( dp[1][2] += dp[0][0] ) → ( dp[1] = [0, 1, 1] )
  • 选择卡牌 2:
    • 选择正面 ( a_2 ):( dp[2][0] += dp[1][1] ) → ( dp[2] = [1, 1, 1] )
    • 选择背面 ( b_2 ):( dp[2][1] += dp[1][1] ) → ( dp[2] = [1, 2, 1] )
  • 选择卡牌 3:
    • 选择正面 ( a_3 ):( dp[3][0] += dp[2][2] ) → ( dp[3] = [1, 2, 2] )
    • 选择背面 ( b_3 ):( dp[3][2] += dp[2][1] ) → ( dp[3] = [1, 2, 4] )

最终,( dp[3][0] = 3 ),表示有 3 种方案使得和对 3 取模为 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 的方案数为 1

    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

    return dp[n][0]

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

代码解读

  1. 初始化

    • dp = [[0] * 3 for _ in range(n + 1)] 创建一个大小为 (n+1)×3 的数组,初始化为 0。
    • dp[0][0] = 1 表示没有卡牌时,和为 0 的方案数为 1。
  2. 状态转移

    • 外层循环遍历每张卡牌,内层循环遍历和对 3 取模的结果。
    • 通过选择正面和背面,更新当前状态。
  3. 返回结果

    • return dp[n][0] 返回最后的结果,即前 ( n ) 张卡牌中,和对 3 取模为 0 的方案数量。

六、个人总结

在解决卡牌翻面求和问题的过程中,我深刻体会到了动态规划的强大和灵活性。这个问题表面上看起来简单,但通过合理的状态定义和转移,可以有效地将复杂度降低到线性级别。

在实现过程中,我注意到动态规划的状态转移需要细心处理,确保每一步的更新都是基于前一步的结果。通过对状态的合理划分,我们能够清晰地理解每张卡牌对最终结果的影响。

此外,我也体会到代码的可读性和注释的重要性。在复杂的逻辑中,清晰的注释可以帮助我和他人更快地理解代码的意图和实现方式。未来在编写代码时,我会更加注重这些方面。

通过这个问题的学习,我对动态规划的理解更加深入,也增强了我解决实际问题的能力。接下来,我会继续练习更多的动态规划问题,以巩固我的学习成果,并探索更复杂的算法和数据结构。