卡牌翻面求和问题 | 豆包MarsCode AI刷题

152 阅读5分钟

问题描述

image.png

问题分析

小M有 ( n ) 张卡牌,每张卡牌的正面和背面分别有不同的数字。我们的目标是选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。这意味着我们需要寻找所有可能的组合,使得最终的和对3取余为0。

动态规划思路

动态规划思路分为以下几个步骤

  1. 定义状态: 我们使用一个长度为3的数组 dp 来记录当前状态。dp[i] 表示当前组合中,对3取余为 i 的组合方式数量。初始时,我们只有一种方式来得到和为0(即什么都不选),所以 dp = [1, 0, 0]
  2. 状态转移: 对于每张卡牌 ( 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] 的值加到相应的新状态中。
  1. 迭代处理每张卡牌: 遍历所有的卡牌,每次使用 dp 更新生成 new_dp,然后将 new_dp 赋值给 dp。
  2. 最终结果: 在所有卡牌处理完成后,返回 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],背面同理。

总结

动态规划简单来说,就是通过将复杂问题分解为更简单的子问题。

一般步骤有:

  1. 状态定义:确定需要用什么变量来表示问题状态。例如,在一个求解序列最大和的问题中,可以使用数组来表示到达每个元素时的最大和。

  2. 状态转移方程:找出当前状态与前一个状态之间的关系,构造出状态转移方程。这个方程描述了如何从已知的状态推导出未知的状态。

  3. 边界条件: 处理基本情况,确保算法能够正确处理最简单的输入情况,例如空数组或只有一个元素的情况。

  4. 计算顺序:根据状态转移方程,选择合适的计算顺序,确保每个状态都能在需要时有效地被计算。

动态规划常常与以下算法相比较:

  • 回溯算法:回溯通常是暴力搜索的策略,适合寻找所有可能解,但时间复杂度往往很高。
  • 贪心算法:贪心算法通过局部最优选择来构建全局最优解,但并不总能保证得到最优解,适合特定问题。
  • 动态规划:在重叠子问题和最优子结构的场景下,动态规划是获得全局最优解的有效方法。

这道题通过动态规划的方法,我们能够有效地计算出在给定卡牌的情况下,所有能使得选中的数字和被3整除的组合数量。在实际应用中,这种方法非常高效,适合处理类似的组合问题。