一、问题背景
想象一下,小M有一堆卡牌,每张卡牌都有正反两面,正面写着一个数字(a_i),反面写着一个数字(b_i)。小M要从每张卡牌中选择一面,把这些选出来的数字加起来,要求这个和能被3整除,然后算一下有多少种不同的选法。因为可能的选法太多了,所以最后结果要对(10^9 + 7)取模,防止数字太大“爆掉”。
二、解题思路 - 动态规划
- 定义状态
- 我们用(dp[i][j])来表示考虑前(i)张卡牌,选出来的数字之和对3取余为(j)的方案数。
- 就好比我们在搭积木,每放一张卡牌((i)增加),就看看我们搭出来的“数字和”对3取余是多少((j)是余数),有几种搭法(方案数)。
- 初始状态
- 最开始一张卡牌都没有的时候,也就是(i = 0),数字和为0的方案有1种,所以(dp[0][0]=1)。这就好比你还没开始搭积木,“什么都没有”这种情况也算一种状态。
- 状态转移方程
- 当我们考虑第(i)张卡牌的时候,这张卡牌有两种选择,选正面(a[i - 1])或者选反面(b[i - 1])。
- 如果我们之前(前(i - 1)张卡牌)已经得到了数字和对3取余为((j - a[i - 1])%3)的方案,那我们选了正面(a[i - 1])后,就会得到数字和对3取余为(j)的方案。
- 同理,如果之前得到了数字和对3取余为((j - b[i - 1])%3)的方案,选了反面(b[i - 1])后,也会得到数字和对3取余为(j)的方案。
- 所以(dp[i][j]=(dp[i - 1][(j - a[i - 1])%3]+dp[i - 1][(j - b[i - 1])%3]))。这里的“(+)”就是把选正面和选反面得到的方案数加起来。
- 最终答案
- 我们把所有(n)张卡牌都考虑完了,也就是(i=n)的时候,数字和对3取余为0的方案数(dp[n][0])就是我们要的答案啦。
- 在计算过程中,每做一次加法,都要对(10^9+7)取模,就像每走一步都要检查一下有没有超出“安全范围”,防止数字大到计算机都处理不了。
三、代码实现 下面是用Python实现的代码:
def solution(n, a, b):
dp = [[0] * 3 for _ in range(n + 1)]
dp[0][0] = 1
for i in range(1, n + 1):
for j in range(3):
dp[i][j] += dp[i - 1][(j - a[i - 1]) % 3]
dp[i][j] += dp[i - 1][(j - b[i - 1]) % 3]
dp[i][j] %= MOD
return dp[n][0]
"""这里的`MOD`就是我们说的\(10^9 + 7\),用来取模的。
`dp`是一个二维列表,用来记录不同状态下的方案数。
通过两层循环,
一层循环遍历卡牌数量,
一层循环遍历余数,不断更新`dp`的值,
最后返回\(dp[n][0]\)就是答案啦。"""