卡牌翻面求和:一个动态规划问题的详细解析
问题描述
给定 n 张卡牌,每张卡牌有正反两面,正面写着数字 a[i],反面写着数字 b[i]。我们需要通过选择每张卡牌的正面或反面,使得所有卡牌数字之和可以被3整除。问题的目标是:计算出所有满足条件的方案总数。
输入与输出:
- 输入:
- 整数
n表示卡牌的数量。 - 数组
a[]和b[],分别表示每张卡牌的正面与背面上的数字。
- 整数
- 输出:
- 输出满足条件的方案数,结果需要对
10^9 + 7取模。
- 输出满足条件的方案数,结果需要对
示例:
样例 1:
输入:
n = 3
a = [1, 2, 3]
b = [2, 3, 2]
输出:
3
解释: 在这个例子中,卡牌的选择方案包括:
- 选择卡牌的正面(1, 2, 2);
- 选择卡牌的正面(1, 3, 2);
- 选择卡牌的正面(1, 2, 2)。
满足条件的方案数量为3。
思路解析
1. 问题归类:动态规划
从题目中可以看出,卡牌的选择并不独立,而是相互影响的。每张卡牌的选择都会影响最终的结果,因此这类问题本质上可以用 动态规划 来求解。我们要解决的问题是:如何将每张卡牌的正面或背面的选择转化为一个动态规划问题,并最终得到满足条件的方案数。
2. 状态定义
为了使用动态规划,我们需要定义一个状态来表示当前的选择情况。我们不关心每一张卡牌的具体选择,而是关心当前所有选择的和对3取余的结果。我们可以定义一个状态数组 dp,其中:
dp[j]表示当前已经选择的卡牌和对3取余结果为j的方案数。j的值可以是 0, 1 或 2(因为我们关心的是对3的余数)。
3. 初始状态
初始时,我们还没有选择任何卡牌,因此和对3取余的结果自然是0,即 dp[0] = 1。也就是说,刚开始没有选择任何卡牌时,我们的方案数只有1种。
4. 状态转移
每一张卡牌 i 都有两个选择:
- 选择卡牌的正面
a[i]; - 选择卡牌的反面
b[i]。
我们要做的,就是更新 dp 数组。对于每一张卡牌的选择,都要基于当前的 dp 数组来更新。具体来说,如果我们已经得到了一个和对3取余为 j 的方案,那么:
- 选择卡牌的正面后,新的和对3取余是
(j + a[i]) % 3; - 选择卡牌的反面后,新的和对3取余是
(j + b[i]) % 3。
这意味着,每个 dp[j] 的值都会影响到后续的选择,因此我们需要用一个新的数组 newDp 来存储新的状态。
5. 最终结果
最终,我们要得到的是所有卡牌的选择中,和对3取余为0的方案数。也就是 dp[0]。
动态规划实现
public class Main {
public static int solution(int n, int[] a, int[] b) {
final int MOD = 1000000007;
// dp[i]表示前i张卡牌中,和对3取余为i的方案数
int[] dp = new int[3];
dp[0] = 1; // 初始状态:没有卡牌时,和被3整除的方案数为1
for (int i = 0; i < n; i++) {
// 记录当前卡牌的选择影响
int[] newDp = new int[3];
for (int j = 0; j < 3; j++) {
// 如果选择正面 a[i]
newDp[(j + a[i]) % 3] = (newDp[(j + a[i]) % 3] + dp[j]) % MOD;
// 如果选择背面 b[i]
newDp[(j + b[i]) % 3] = (newDp[(j + b[i]) % 3] + dp[j]) % MOD;
}
// 更新dp数组
dp = newDp;
}
// 返回和为3的倍数的方案数,即 dp[0]
return dp[0];
}
public static void main(String[] args) {
System.out.println(solution(3, new int[]{1, 2, 3}, new int[]{2, 3, 2}) == 3);
System.out.println(solution(4, new int[]{3, 1, 2, 4}, new int[]{1, 2, 3, 1}) == 6);
System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 32);
}
}
代码详解:
dp[0] = 1:表示没有卡牌时,和为0的方案数为1。newDp[(j + a[i]) % 3]和newDp[(j + b[i]) % 3]:这两行代码更新了新的状态,其中a[i]和b[i]分别表示当前卡牌的正面和反面数字,通过取模运算来确保结果符合对3取余的要求。dp = newDp:在每一轮更新后,我们将newDp的值赋给dp,以便进入下一张卡牌的选择。
时间与空间复杂度:
- 时间复杂度:每次循环中,我们更新
dp数组的三个状态,因此时间复杂度是O(n),其中n是卡牌的数量。 - 空间复杂度:我们使用了一个固定大小为3的数组
dp,所以空间复杂度是O(1)。
思考与优化
- 状态压缩:在此问题中,我们只关心和对3的余数,因此
dp数组的大小仅为3,可以节省空间。通过这种方式,我们在处理类似的问题时,可以降低空间复杂度。 - 模运算的重要性:由于最终的结果需要对
10^9 + 7取模,我们在每一步的状态转移中都使用了模运算,确保数值不会溢出。这在处理大数问题时非常重要。 - 动态规划的核心思想:此问题实际上是经典的背包问题的变种,我们的背包容量是3(即我们关心的状态是0, 1, 2)。通过动态规划,我们能够高效地求解出符合条件的方案数。
总结
通过这道卡牌翻面求和问题,我们深入探讨了动态规划的核心思想及其应用。通过定义状态、状态转移和最优解的计算,我们能够在合理的时间复杂度内解决问题。在实际编程过程中,动态规划的思想可以帮助我们高效解决许多复杂的优化问题。希望这篇文章能帮助大家更好地理解和掌握动态规划的基本技巧。