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

57 阅读4分钟

问题描述

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

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

方法背景

动态规划(Dynamic Programming,简称DP)是一种算法思想,用于解决具有重叠子问题和最优子结构特性的问题。动态规划通常用于求解最优化问题,特别是在图论中的最短路径问题、网络流问题、资源分配问题等。

动态规划的关键步骤通常包括:

• 定义状态:确定状态是什么,以及状态如何影响问题的解。

• 确定状态转移方程:找出如何从一个状态转移到下一个状态。

• 确定边界条件:确定问题的基本情况,即最小的子问题的解。

• 实现算法:根据上述定义,实现算法,通常涉及填充一个表格,表格的行和列代表不同的状态。

• 计算最终结果:从填充好的表格中提取问题的解。

在本题中我们将使用状态规划进行求解。

程序分析

要使用动态规划,首先我们需要定义状态来表示问题。我们可以定义一个二维数组dp[i][j],其中i表示考虑的卡牌数量,j表示当前所有选择的数字之和除以3的余数。dp[i][j]的值表示在考虑前i张卡牌时,使得所有选择的数字之和除以3余数为j的方案数量。

我们有以下状态转移方程:

[dp[i][j]=dp[i1][j]+dp[i1][jai]+dp[i1][j+bi][dp[i][j]=dp[i-1][j]+dp[i-1][j-a_i]+dp[i-1][j+b_i]

其中a_ib_i分别是第i张卡牌正面和背面的数字。 由于我们在数据较的情况下需要对10910^9级别的数据取模,需要在计算过程中不断取模以避免整数溢出。

下面是详细的步骤:

• 初始化一个大小为(n+1) x 3的数组dp,所有元素初始化为0。dp[0]初始化为1,因为当没有卡牌时,只有一种方案使得模3的余数为0。

• 遍历每张卡牌,对于每张卡牌,更新dp数组。对于每张卡牌的正面和背面数字,更新dp数组中对应的余数状态。

• 在更新dp数组时,需要对每个状态的值进行取模操作。

• 最后,dp[0]的值就是所有方案的数量,因为我们需要和为3的倍数。

测试样例

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

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

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

代码实现

def solution(n: int, a: list, b: list) -> int:
    #定义最大数据上限
    MOD = 10**9 + 7 
    
    # 初始化动态规划数组
    dp = [0, 0, 0]
    dp[0] = 1  # 初始时,和为0的方案数为1
 
    for i in range(n):
        # 计算当前卡牌选择后的新方案数
        new_dp = [0, 0, 0]
        
        for r in range(3):
            # 对于每个余数r,可以选择a[i]或b[i]
            new_dp[(r + a[i]) % 3] = (new_dp[(r + a[i]) % 3] + dp[r]) % MOD
            new_dp[(r + b[i]) % 3] = (new_dp[(r + b[i]) % 3] + dp[r]) % MOD
        
        # 更新dp数组
        dp = new_dp
 
    # dp[0]就是我们需要的方案数
    return dp[0]
 
if __name__ == '__main__':
    print(solution(n = 3, a = [1, 2, 3], b = [2, 3, 2]) == 3)  # Output: 3
    print(solution(n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1]) == 6)  # Output: 6
    print(solution(n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5]) == 32)  # Output: 32
   

详细解析

初始化动态规划数组:

dp = [0, 0, 0]:这个数组用来存储当前数字之和除以3的余数对应的方案数。dp[0]表示当前和除以3余0的方案数,dp[1]表示余1的方案数,dp[2]表示余2的方案数。初始状态下,只有dp[0]为1,因为没有任何卡牌被选中时,总和为0,这是一个有效的方案。

遍历卡牌:

for i in range(n):这个循环遍历每一张卡牌,i代表当前卡牌的索引。

计算新的方案数:

new_dp = [0, 0, 0]:在每次迭代中,我们创建一个新的new_dp数组来存储更新后的方案数。

for r in range(3):这个内层循环遍历所有可能的余数(0,1,2),代表当前总和除以3的余数。

new_dp[(r + a[i]) % 3] = (new_dp[(r + a[i]) % 3] + dp[r]) % MOD:如果选择当前卡牌的正面,我们将更新余数为(r + a[i]) % 3的方案数。这里使用取模操作来确保余数在0到2之间。

new_dp[(r + b[i]) % 3] = (new_dp[(r + b[i]) % 3] + dp[r]) % MOD:同样,如果选择当前卡牌的背面,我们也会更新余数为(r + b[i]) % 3的方案数。

更新动态规划数组:

dp = new_dp:在每次迭代结束后,我们将new_dp的值赋给dp,这样dp就包含了考虑当前卡牌后的最新方案数。

时间复杂度

• 外层循环:函数中有一个外层循环,它遍历所有的卡牌,循环次数为n(卡牌的数量)。

• 内层循环:对于每张卡牌,有一个内层循环,它遍历所有可能的余数(0,1,2),循环次数为3。

由于外层循环和内层循环都是固定的,每次迭代都执行固定数量的操作,因此总的时间复杂度是O(n),其中n是卡牌的数量。

空间复杂度

• 动态规划数组:函数中使用了一个长度为3的数组dp来存储中间结果。这个数组的大小不随卡牌数量n的变化而变化,因此是常数空间。

• 临时数组:在每次迭代中,我们还使用了一个临时数组new_dp,其大小也是固定的,为3。

由于我们只使用了一个固定大小的数组,所以空间复杂度是O(1),即常数空间复杂度。

综上所述,给定的solution函数的时间复杂度是O(n),空间复杂度是O(1)。这意味着随着卡牌数量的增加,函数的运行时间将线性增长,但所需的额外空间保持不变。