卡牌翻面求和问题 | python解法
题目分析
问题简述
我们需要选择每张卡牌的正反面,使得所有选择的卡牌的数字和可以被3整除。对于每张卡牌,正反面的数字不同,因此每张卡牌的选择有两个可能性,即正面朝上或背面朝上。问题的目标是计算所有可能的选择方式中,使得数字和能被3整除的组合数。
给定条件中,数字可能会很大,导致可能的选择方式数量非常庞大,因此最终结果需要对 10^9 + 7取模。
解题思路
为了高效地求解问题,可以通过 动态规划 (Dynamic Programming, DP) 的方法来处理。
动态规划的核心思想
我们可以使用一个三元组的 dp 数组,来表示某一时刻选取的卡牌的数字和的模3余数。具体地,dp[0] 代表和对3取模为0的组合数,dp[1] 代表和对3取模为1的组合数,dp[2] 代表和对3取模为2的组合数。
每当我们处理一张新卡牌时,我们需要根据当前卡牌的正面和背面的模3余数来更新这个 dp 数组。
详细步骤
-
初始化状态 初始时,数字和为0的组合只有一种情况,即没有选择任何卡牌时,和为0。因此,
dp[0] = 1,而dp[1] = dp[2] = 0。 -
迭代处理每张卡牌 对于每一张卡牌,我们首先计算其正面和背面数字对3的余数,即
rem_a = a[i] % 3和rem_b = b[i] % 3。然后,我们根据当前的
dp数组来更新每一种余数的组合数。具体地,假设当前有一组组合数dp[j],那么:- 选择正面朝上的情况会使得当前和的模3余数增加
rem_a。 - 选择背面朝上的情况会使得当前和的模3余数增加
rem_b。
对每一张卡牌,我们会根据这两个选择更新一个临时数组
temp,并将其应用到dp中。 - 选择正面朝上的情况会使得当前和的模3余数增加
-
更新状态 更新时,我们不直接修改
dp数组,而是使用一个临时数组temp来存储当前卡牌处理后的结果。这样可以避免更新时的干扰问题。 -
最终结果 处理完所有卡牌后,
dp[0]就是和为0的组合数,也即和能被3整除的组合数。
时间复杂度
- 每张卡牌的处理需要更新
dp数组,而dp数组的大小固定为3。因此,每张卡牌的处理时间复杂度为O(1)。 - 总共有
n张卡牌,因此整体时间复杂度为 O(n)。
空间复杂度
- 我们只使用了一个大小为3的
dp数组来存储每种余数的组合数,因此空间复杂度为 O(1)。
代码实现
def solution(n: int, a: list, b: list) -> int:
MOD = 10**9 + 7
# dp[i] 表示和对3取模为i的组合数
dp = [1, 0, 0] # 初始时只有和为0的组合数为1
for i in range(n):
# 临时保存当前轮次的结果
temp = [0, 0, 0]
# 计算正面和反面卡片数值的模3余数
rem_a = a[i] % 3
rem_b = b[i] % 3
# 更新 dp 数组
for j in range(3):
temp[(j + rem_a) % 3] = (temp[(j + rem_a) % 3] + dp[j]) % MOD
temp[(j + rem_b) % 3] = (temp[(j + rem_b) % 3] + dp[j]) % MOD
# 将临时计算结果应用到 dp
dp = temp
# 返回和能被3整除的组合数
return dp[0]
拓展:背包问题
这道题属于 动态规划中的 "状态压缩" 或 "子集状态转移" 类型的问题,具体来说,它可以归类为 "背包问题" 的变种,特别是与 模运算相关的背包问题。这种问题通常通过 "状态压缩" 的方式来处理,因为我们只需要跟踪一小部分状态,而不是所有可能的状态。
动态规划的状态转移
-
背包问题的核心思想
- 背包问题通常会有一个或多个物品,每个物品都有一定的权值或代价(例如这里的卡牌的数字),我们需要在有限的资源(背包容量)下,选择一些物品使得某些条件最优(例如最大价值、最小成本、可被某个数整除的组合等)。
- 在这道题中,背包容量被模3约束,因为我们关心的是所有选择的卡牌的数字和对3取模后的值。
-
状态表示
- 在标准的背包问题中,我们用一个数组
dp[i]表示选择某些物品后,背包容量为i时的最优值。而在这个问题中,我们使用一个dp[3]数组,其中dp[i]表示当前卡牌选择后的数字和对3取模后的余数为i的组合数。
- 在标准的背包问题中,我们用一个数组
-
转移方程
- 每张卡牌有两种选择:正面或背面。正面的数字和对3的余数是
a[i] % 3,背面的数字和对3的余数是b[i] % 3。 - 我们根据前一张卡牌的状态(即当前数字和的模3值),通过这两种选择来更新当前的
dp数组。
- 每张卡牌有两种选择:正面或背面。正面的数字和对3的余数是
-
空间压缩
- 本题巧妙地利用了 "状态压缩"。通常背包问题需要维护一个二维的状态数组来表示每个状态转移,但由于我们只关心每个状态对3的余数,状态空间可以压缩为一个大小为3的数组,表示每个模3余数对应的组合数。
- 这大大减少了空间复杂度,只需要存储当前的
dp数组,并通过一个临时数组temp来更新。
这道题和背包问题的异同:
-
相似点:
- 都是通过选择物品来满足一定条件,且在选择时会影响总和(在背包问题中是总价值,或在这道题中是模3的余数)。
- 都可以通过动态规划的方式来解决,定义一个状态数组来表示当前的选择情况,然后通过转移方程来更新状态。
-
不同点:
- 背包问题通常关注的是最大值或最小值,而这道题关注的是能被3整除的组合数。目标并不是优化某个量,而是符合模3约束。
- 背包问题通常有明确的容量或重量限制,而本题没有类似的容量限制,而是与数字和的模3余数相关。
其他相关类型
- 模运算问题 这类问题通常会涉及到对某个数(如和、差、积等)取模并要求满足某些条件。在本题中,所有卡牌的选择组合数需要满足数字和能被3整除,也就是和对3取模为0。这类问题常见于处理大规模数据时,避免溢出并保持结果在一定范围内。
- 区间背包问题 如果卡牌的数量很大,可以考虑将问题转化为区间背包问题。通过子集枚举和状态转移,可以进一步优化。
- 组合优化问题 本题是通过动态规划计算所有可能组合的模3余数的数量,属于组合优化问题中的一种。
总结
这道题的核心是利用动态规划计算卡牌选择的组合数,使得所有选出的卡牌的数字和对3取模结果为0。通过三元组的 dp 数组来跟踪和的模3余数,并通过滚动数组的方式更新每一张卡牌的选择情况,能够高效地解决问题。