题目解析:卡牌反面求和问题 | 豆包MarsCode AI刷题

41 阅读4分钟

问题描述

小M有 nn 张卡牌,每张卡牌的正反面分别写着不同的数字,正面是 ai​,背面是 bi​。小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,结果需要对 10^9+7 取模。

例如:如果有3张卡牌,正反面数字分别为 (1,2)(2,3) 和 (3,2),你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。


测试样例

样例1:

输入:n = 3 ,a = [1, 2, 3] ,b = [2, 3, 2]
输出:3

样例2:

输入:n = 4 ,a = [3, 1, 2, 4] ,b = [1, 2, 3, 1]
输出:6

样例3:

输入:n = 5 ,a = [1, 2, 3, 4, 5] ,b = [1, 2, 3, 4, 5]
输出:32

这是一道经典动态规划的题目,结合了动态规划与数论的知识,难度不算大,但是是非常经典的动态规划的问题,对于我们上手熟悉动态规划算法是非常合适的。

看题目,简单来说就是一个组合优化的问题,目的是要找出所有符合特定条件的组合,即和模3为0,每张卡牌只有两个值ai和bi,我们需要对总和模3进行分类统计,最终找到总和模3为0的所有方案数,但是卡牌数量非常多,直接暴力枚举所有组合的话大概率会超时,因此要想其他方法,最先想到的肯定是动态规划,就是通过状态转移逐步构建更复杂情况下的解。

我们定义的状态:dp[i][mod]表示在前i张卡牌中,选择某些正面或反面,使得总和模3等于mod的方案数,对于每张卡牌i,正面数字是a[i],反面数字是b[i],当我们遍历到第i张牌的时候,可以选择它的正面或者反面向上,如果当前总和模3为j的话,选择正面后,总和变为:(j + a[i]) % 3,同理,选择反面后,总和变为(j + b[i]) % 3,。因此状态转移方程定义为:

dp[i][(j + a[i]) % 3] += dp[i - 1][j]

dp[i][(j + b[i]) % 3] += dp[i - 1][j]

对于第一张牌,我们有两种选择,选择正面:dp[1][a[0] % 3] = 1.选择反面:dp[1][b[0] % 3] = 1,我们在计算完所有卡牌之后,答案就是dp[n][0],同时不要忘记每次累加时要对10^9+7取模。贴上代码:

const int MOD = 1e9 + 7;
int solution(int n, std::vector<int> a, std::vector<int> b) {
    vector<vector<int>> dp(n + 1, vector<int>(3, 0));
    dp[1][a[0]%3] += 1;
    dp[1][b[0]%3] += 1;

    // 状态转移
    for (int i=2; i<=n; ++i) {
        for (int j=0; j<3; ++j) {
            if (dp[i-1][j]>0) {
                dp[i][(j+a[i-1])%3]=(dp[i][(j+a[i-1])%3]+dp[i-1][j])%MOD;
                dp[i][(j+b[i-1])%3]=(dp[i][(j+b[i-1])%3]+dp[i-1][j])%MOD;
            }
        }
    }
    return dp[n][0];
}

复杂度分析:外层遍历遍历n张卡牌,内层循环处理3个模数状态,复杂度为 O(n)。这一类问题的核心就是如何动态管理取模后的状态,他们的本质都是找到适合的状态标识和状态转移方法,通过将问题抽象为状态集合,找出状态之间的联系,我们还可以通过使用滚动数组来对时间复杂度进行进一步优化,可以将本体的时间复杂度优化到常数级,这在动态规划中也是一种非常常见的优化手段。 这道题目结合了动态规划和数论中的模运算性质,展示了如何通过状态转移解决复杂的组合问题。在解题过程中,我们不仅学习了动态规划的应用,还提升了对数学问题建模的能力。希望这篇博客能帮助你更好地理解和解决类似问题!