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

151 阅读6分钟

问题描述 :

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

问题分析:

我们需要计算所有可能的组合,使得这些组合的数字之和可以被3整除。

数据结构选择:

我们可以使用一个二维数组 dp,其中 dp[i][j] 表示前 i 张卡牌中,选择某些卡牌使得它们的和模3余 j 的方案数。 状态转移: 对于每一张卡牌,我们可以选择它的正面或背面。 更新 dp 数组时,考虑当前卡牌的正面和背面数字对当前状态的影响。

二维数组:

二维数组可以看作是一种特殊的数组,它的每个元素又是一个数组。简单来说,它就像是一个表格,有行和列的概念。例如,我们可以用二维数组来表示一个矩阵,棋盘等具有二维结构的数据。

在计算机的内存中,二维数组有两种常见的存储方式:

1. 按行优先存储:将二维数组的每一行看作一个一维数组,依次将各行存放在连续的内存单元中。先存储第 0 行,再存储第 1 行,依此类推。比如对于一个二维数组int a[3][4](表示 3 行 4 列的二维整数数组),按行优先存储时,内存中存放的顺序就是先存放第 0 行的 4 个元素,然后是第 1 行的 4 个元素,最后是第 2 行的 4 个元素。

2. 按列优先存储:把二维数组的每一列看作一个一维数组,依次将各列存放在连续的内存单元中。先存储第 0 列,再存储第 1 列,等等。同样对于int a[3][4],按列优先存储时,会先存放第 0 列的 3 个元素,接着是第 1 列的 3 个元素,以此类推。

3.优点: 直观地表示具有二维结构的数据,符合人们对表格、矩阵等概念的理解。 方便进行与二维结构相关的各种运算和操作,如矩阵运算、遍历二维平面上的数据等。

4. 缺点: 内存空间浪费:如果二维数组中存在大量未使用的元素,会占用不必要的内存空间。例如,当需要表示一个稀疏矩阵(大部分元素为 0 的矩阵)时,直接使用二维数组存储可能不是最经济的方式。 灵活性相对较差:一旦定义了二维数组的行数和列数,在程序运行过程中通常难以动态改变其大小,需要重新定义新的数组并进行数据的复制等操作来实现类似的效果。

代码如下: 

public class Main {    public static int solution(int n, int[] a, int[] b) {        final int MOD = 1000000007;// dp[i][j] 表示前 i 张卡牌中,选择某些卡牌使得它们的和模3余 j 的方案数        int[][] dp = new int[n + 1][3];         // 初始化:前0张卡牌,和为0的方案数为1    dp[0][0] = 1;        // 遍历每一张卡牌    for (int i = 1; i <= n; i++) {        // 当前卡牌的正面和背面数字        int front = a[i - 1];        int back = b[i - 1];                // 更新 dp 数组        for (int j = 0; j < 3; j++) {            // 选择正面的情况            dp[i][(j + front) % 3] = (dp[i][(j + front) % 3] + dp[i - 1][j]) % MOD;            // 选择背面的情况            dp[i][(j + back) % 3] = (dp[i][(j + back) % 3] + dp[i - 1][j]) % MOD;        }    }        // 返回前 n 张卡牌中,和模30的方案数    return dp[n][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);    }}

学习心得:

1.理解问题本质

卡牌翻面求和问题看似简单,实则需要深入理解其背后的逻辑。关键在于把握卡牌的两种状态(正面和背面)以及翻面操作如何影响最终的求和结果。每一次翻面都可能改变某张卡牌在求和时所贡献的数值,所以清晰认识到这一点是解决问题的基础。

2.算法思路的多样性

通过学习,我发现解决这类问题可以有多种算法思路。比如直接模拟法,它就像按部就班地按照题目描述去实际操作卡牌的翻面过程,然后再计算总和,这种方法很直观,容易理解,但可能在处理大规模数据时效率不高。而状态计数法就相对巧妙一些,它不关注具体的翻面过程,而是通过统计每张卡牌翻面的次数来确定最终状态,进而计算求和,这种思路在一定程度上简化了问题的处理过程。还有位运算优化的模拟法,利用位运算的特性来更高效地处理卡牌状态和求和,不过这需要对编程语言的位运算有较好的掌握。这让我认识到,面对一个问题,多角度思考算法思路是很有必要的,不同的思路可能在不同的场景下各有优劣。

3.数据结构的重要性

在解决卡牌翻面求和问题时,选择合适的数据结构对解题效率有着重要影响。比如用数组来记录卡牌的正面数字、背面数字以及状态就比较方便,能够很直观地进行索引和操作。但如果问题规模较大,可能需要考虑更高效的数据结构,像哈希表等,来加快查询和更新的速度。这让我明白,根据问题的具体情况合理选用数据结构是提高算法性能的关键环节之一。

既然这类问题涉及到多种算法和数据结构,那么就有必要深入学习它们。比如要深入理解直接模拟法、状态计数法等算法的具体实现步骤以及它们的优缺点;对于数据结构,要了解数组、哈希表等的特性、适用场景以及如何在具体问题中运用它们。只有对这些知识有扎实的掌握,才能在遇到这类问题时迅速做出准确的判断和高效的解决方案。