问题描述
问题分析
小M有 ( n ) 张卡牌,每张卡牌的正面和背面分别有不同的数字。我们的目标是选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。这意味着我们需要寻找所有可能的组合,使得最终的和对3取余为0。
动态规划思路
动态规划思路分为以下几个步骤
- 定义状态:
我们使用一个长度为3的数组 dp 来记录当前状态。
dp[i]表示当前组合中,对3取余为 i 的组合方式数量。初始时,我们只有一种方式来得到和为0(即什么都不选),所以dp = [1, 0, 0]。 - 状态转移:
对于每张卡牌 ( i ),我们可以选择它的正面数字 (
a[i]) 或背面数字 (b[i])。 对于每种现存组合(即当前的 dp 状态),我们会更新新的状态 new_dp。
新状态的值计算如下:
- 如果当前组合对3取余为 j,选择正面:新组合的和对3取余为
(j + a[i]) % 3。 - 如果当前组合对3取余为 j,选择背面:新组合的和对3取余为
(j + b[i]) % 3。 所以对于每个 j ,我们会将dp[j]的值加到相应的新状态中。
- 迭代处理每张卡牌: 遍历所有的卡牌,每次使用 dp 更新生成 new_dp,然后将 new_dp 赋值给 dp。
- 最终结果:
在所有卡牌处理完成后,返回
dp[0],这就是所有能使总和被3整除的组合数。
具体代码实现
function solution(n, a, b) {
const MOD = 10 ** 9 + 7;
// dp[i] 表示和对3取模为i的组合方式数
let dp = [1, 0, 0]; // 初始状态,和为0的组合方式有1种
for (let i = 0; i < n; i++) {
// 获取当前卡牌的正面与背面
const front = a[i];
const back = b[i];
// 计算当前卡牌选择后的新的 dp 状态
let new_dp = [0, 0, 0];
for (let j = 0; j < 3; j++) {
// 更新新状态
new_dp[(j + front) % 3] = (new_dp[(j + front) % 3] + dp[j]) % MOD;
new_dp[(j + back) % 3] = (new_dp[(j + back) % 3] + dp[j]) % MOD;
}
dp = new_dp; // 更新 dp 数组
}
// 返回和能被3整除的方案数
return dp[0];
}
function main() {
console.log(solution(3, [1, 2, 3], [2, 3, 2]) === 3);
console.log(solution(4, [3, 1, 2, 4], [1, 2, 3, 1]) === 6);
console.log(solution(5, [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]) === 32);
}
main();
由于可能的方案数量过大,结果需要对 109+7109+7 取模,先定义了一个常量 MOD,用于取模运算。
然后初始化了 dp 数组,表示当前和对3取模为0、1、2的组合方式数。开始遍历每一张卡牌,使用const front = a[i];和const back = b[i];用来获取当前卡牌的正面和背面数字,再初始化一个新的 new_dp 数组,用于存储当前卡牌选择后的新状态,开始遍历当前 dp 数组的每个状态,new_dp[(j + front) % 3] = (new_dp[(j + front) % 3] + dp[j]) % MOD; 和new_dp[(j + back) % 3] = (new_dp[(j + back) % 3] + dp[j]) % MOD;更新 new_dp 数组,分别考虑选择当前卡牌的正面数字和背面数字。dp = new_dp;将 new_dp 赋值给 dp,更新 dp 数组,最后return出dp[0],返回和能被3整除的方案数。
状态转移逻辑:
当前状态:假设我们以前有一个组合,其和对3的余数为 j,即 dp[j] 表示这样的组合数量。
选择当前卡牌:对于当前卡牌 ( i ),我们可以选择其正面 (front) 或背面 (back) 的数字。
更新新状态:当我们选择了正面(数字为 front)后,那么这个组合的总和就变成了原来的和加上 front。新的和对3取余的结果是 (j + front) % 3。因此,我们需要增加到 new_dp[(j + front) % 3],背面同理。
总结
动态规划简单来说,就是通过将复杂问题分解为更简单的子问题。
一般步骤有:
-
状态定义:确定需要用什么变量来表示问题状态。例如,在一个求解序列最大和的问题中,可以使用数组来表示到达每个元素时的最大和。
-
状态转移方程:找出当前状态与前一个状态之间的关系,构造出状态转移方程。这个方程描述了如何从已知的状态推导出未知的状态。
-
边界条件: 处理基本情况,确保算法能够正确处理最简单的输入情况,例如空数组或只有一个元素的情况。
-
计算顺序:根据状态转移方程,选择合适的计算顺序,确保每个状态都能在需要时有效地被计算。
动态规划常常与以下算法相比较:
- 回溯算法:回溯通常是暴力搜索的策略,适合寻找所有可能解,但时间复杂度往往很高。
- 贪心算法:贪心算法通过局部最优选择来构建全局最优解,但并不总能保证得到最优解,适合特定问题。
- 动态规划:在重叠子问题和最优子结构的场景下,动态规划是获得全局最优解的有效方法。
这道题通过动态规划的方法,我们能够有效地计算出在给定卡牌的情况下,所有能使得选中的数字和被3整除的组合数量。在实际应用中,这种方法非常高效,适合处理类似的组合问题。