一、题目背景
小M有 𝑛 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 𝑎𝑖,背面是 𝑏𝑖。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 10^9+7 取模。
二、解决思路
通过动态规划来解决。我们可以使用一个 dp 数组,dp[i] 代表当前的和对 3 取模的结果。我们需要依次考虑每张卡牌的选择,更新 dp 数组的状态。
-
动态规划状态定义
我们可以定义 dp[j] 为当前已选择的卡牌数字和对 3 取模为 j 的方案数。即,dp[0] 表示和对 3 取模为 0 的方案数,dp[1] 表示和对 3 取模为 1 的方案数,dp[2] 表示和对 3 取模为 2 的方案数。
-
状态转移
对于每张卡牌 i,我们有两个选择:
- 选择卡牌的正面
a[i],这会改变当前和对 3 取模的值。 - 选择卡牌的背面
b[i],这也会改变当前和对 3 取模的值。
假设当前我们已经计算出了前 i-1 张卡牌的方案数,并且我们已经知道 dp 数组的状态,那么对于卡牌 i,我们可以更新新的状态 dp'(存储当前卡牌 i 之后的状态):
-
对于每个可能的当前状态
j(即dp[j]),我们有两种选择:- 如果选择正面
a[i],则新的状态是(j + a[i]) % 3。 - 如果选择背面
b[i],则新的状态是(j + b[i]) % 3。
- 如果选择正面
三、代码实现与解析
def solution(n: int, a: list, b: list) -> int:
MOD = 10**9 + 7 # 模数
dp = [0] * 3 # dp数组,用于记录和对3取模的方案数
dp[0] = 1 # 和为0的方案数初始化为1
for i in range(n):
new_dp = [0] * 3 # 临时新dp数组,保存更新后的方案数
for j in range(3):
if dp[j] > 0:
# 选择正面
new_dp[(j + a[i]) % 3] = (new_dp[(j + a[i]) % 3] + dp[j]) % MOD
# 选择背面
new_dp[(j + b[i]) % 3] = (new_dp[(j + b[i]) % 3] + dp[j]) % MOD
dp = new_dp # 更新dp为新的dp数组
return dp[0] # 最终结果是和为0的方案数,即dp[0]
# 测试用例
if __name__ == '__main__':
print(solution(n = 3, a = [1, 2, 3], b = [2, 3, 2])) # 输出3
print(solution(n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1])) # 输出6
print(solution(n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5])) # 输出32
代码解析
-
初始化
dp数组
dp数组的大小为 3,因为我们只关心和对 3 取模的结果。dp[0]表示当前和对 3 取模为 0 的方案数,dp[1]表示当前和对 3 取模为 1 的方案数,dp[2]表示当前和对 3 取模为 2 的方案数。我们初始化dp[0] = 1,表示没有卡牌时,和为 0 的方案数是 1(空集合)。 -
遍历每张卡牌
对于每一张卡牌,我们需要根据选择正面或背面来更新dp数组:对于当前的每个
dp[j](表示当前和对 3 取模为j的方案数),如果我们选择卡牌的正面,新的和为(j + a[i]) % 3,如果选择背面,新的和为(j + b[i]) % 3。 -
状态转移
我们使用一个新的new_dp数组来存储更新后的状态。更新过程是累加的,因为每个状态j可以通过两种选择(正面或背面)得到两个新的状态。 -
最终结果
经过所有卡牌的处理后,dp[0]就表示所有选择中和对 3 取模为 0 的方案数。这个值就是最终的结果。 -
取模
每次更新dp数组时,都要对 109+7109+7 取模,以防止数值过大。
四、复杂度分析
- 时间复杂度:
我们对每张卡牌都遍历一次,每次更新dp数组的大小为 3。因此,总的时间复杂度为O(n),其中n是卡牌的数量。 - 空间复杂度:
我们使用了一个大小为 3 的数组dp来存储每个和对 3 取模的方案数。因此空间复杂度为O(3),即常数空间。
五、总结
通过动态规划,我们有效地解决了这个问题。使用一个大小为 3 的数组 dp 来记录每一步和对 3 取模的不同方案数,并通过状态转移实现从一个卡牌到下一个卡牌的过渡。最终,我们得到了所有和对 3 取模为 0 的方案数。
在之前,我并不能很好地理解动态规划的题目,只是大概了解了状态转移的概念。尤其是在书写转移方程时,我常常会出错,导致无法顺利解决问题。然而,在这次通过AI刷题的过程中,我逐步理解了动态规划的核心思想,特别是在AI的帮助下,我一步步跟着分析问题,理清了状态转移的过程。通过不断练习,我逐渐能够准确地编写转移方程,并最终完整地解决问题。