问题描述
小M面对一组从 1 到 9 的数字,这些数字被分成多个小组,并从每个小组中选择一个数字组成一个新的数。目标是使得这个新数的各位数字之和为偶数。任务是计算出有多少种不同的分组和选择方法可以达到这一目标。
numbers: 一个由多个整数字符串组成的列表,每个字符串可以视为一个数字组。小M需要从每个数字组中选择一个数字。
例如对于[123, 456, 789],14个符合条件的数为:147 149 158 167 169 248 257 259 268 347 349 358 367 369。
测试样例
样例1:
输入:
numbers = [123, 456, 789]
输出:14
样例2:
输入:
numbers = [123456789]
输出:4
样例3:
输入:
numbers = [14329, 7568]
输出:10
-
思路: 这个问题要求我们从每个数字组中选择一个数字,使得最终选择的数字之和为偶数。我们可以将问题分解为两个状态:当前选择的数字之和为偶数(记为
even)和当前选择的数字之和为奇数(记为odd)。对于每个数字组,我们需要计算选择奇数和偶数后的新状态,并更新even和odd。
图解: 我们可以画出一个状态转移图来表示这个过程。对于每个数字组,我们有两个选择:奇数或偶数。选择奇数会将even状态转变为odd状态,反之亦然。最终,我们需要的是even状态的数量,因为只有这样才能保证所有选择的数字之和为偶数。
`
public class Main {
public static int solution(int[] numbers) {
int even = 1; // 选择偶数的组合
int odd = 0; // 选择奇数的组合数
for (int number : numbers) {
int groupEvenCount = 0; // 当前组中偶数的数量
int groupOddCount = 0; // 当前组中奇数的数量
while (number > 0) {
int digit = number % 10;
if (digit % 2 == 0) {
groupEvenCount++;
} else {
groupOddCount++;
}
number /= 10;
}
int newEven = even * groupEvenCount + odd * groupOddCount; // 新的偶数组合数
int newOdd = even * groupOddCount + odd * groupEvenCount; // 新的奇数组合数
even = newEven;
odd = newOdd;
}
return even; // 返回偶数组合数
}
}
上述代码解决的问题是一个组合问题,而不是传统意义上的动态规划问题。代码的目的是计算从每个数字组中选择一个数字,使得这些数字的总和为偶数的不同选择方法的数量。这个问题可以通过动态规划的思想来解决,但实际上它更接近于组合数学中的计数问题。
算法原理:
-
状态定义:
even:表示当前选择的数字之和为偶数的组合数。odd:表示当前选择的数字之和为奇数的组合数。
-
状态转移:
-
对于每个数字组,我们需要决定是从该组中选择一个奇数还是一个偶数。
-
如果我们选择一个偶数,那么:
- 之前是
even状态的,加上这个偶数后仍然是even状态。 - 之前是
odd状态的,加上这个偶数后会变成even状态。
- 之前是
-
如果我们选择一个奇数,那么:
- 之前是
even状态的,加上这个奇数后会变成odd状态。 - 之前是
odd状态的,加上这个奇数后仍然是odd状态。
- 之前是
-
-
状态更新:
- 对于每个数字组,我们根据该组中奇数和偶数的数量,更新
even和odd的状态。具体来说,我们将even乘以该组中偶数的数量,将odd乘以该组中奇数的数量,然后将这两个结果相加,得到新的even状态。同样,我们也计算新的odd状态。
- 对于每个数字组,我们根据该组中奇数和偶数的数量,更新
-
边界条件:
- 初始时,
even = 1和odd = 0,因为我们还没有做任何选择,所以默认总和为偶数的组合数为1(即没有选择任何数字)。
- 初始时,
-
最终结果:
- 最后,我们只关心总和为偶数的组合数,因此返回
even的值。 - 在这段代码中,我们遍历每个数字组,计算该组中奇数和偶数的数量,并根据这些数量更新
even和odd状态。这个过程重复进行,直到所有数字组都被处理完毕。最后,我们返回even状态的值,即所有选择的和为偶数的组合数。这个算法的时间复杂度是O(n * m),其中n是数字组的数量,m是每个数字组中数字的最大位数。
- 最后,我们只关心总和为偶数的组合数,因此返回
知识总结
新知识点:
- 状态转移方程:在解决动态规划问题时,理解如何根据当前状态推导出下一个状态是关键。
- 奇偶性问题:理解奇偶性问题的性质,如何通过选择奇数或偶数来控制总和的奇偶性。
- 动态规划(Dynamic Programming,简称DP):是一种在数学、管理科学、计算机科学、经济学和生物信息学等领域中使用的,通过把原问题分解为相对简单的子问题的方式解决复杂问题的方法。
动态规划问题具有以下几个典型特征:
- 重叠子问题:问题的解决方案可以通过解决其子问题的解决方案来构建。这意味着在递归方法中,相同的子问题会被多次计算。
- 最优子结构:一个问题的最优解包含其子问题的最优解。也就是说,子问题的最优解可以用来构建原问题的最优解。
- 无后效性:一旦某个状态被确定,它就不受之后决策的影响。换句话说,未来的状态不依赖于过去的状态,而只依赖于当前的状态。
动态规划通常用于解决以下类型的问题:
- 计数问题:如计算不同路径的数量、不同分割方式的数量等。
- 最优化问题:如寻找最长递增子序列、最小编辑距离、最大子数组和等。
- 资源分配问题:如背包问题、硬币找零问题等。
动态规划的解决方案通常涉及以下几个步骤:
- 定义状态:确定状态是什么,以及状态之间的关系。
- 建立状态转移方程:根据问题的特点,推导出从一个状态到另一个状态的转移方式。
- 确定边界条件:确定问题的基本情况,也就是递归的终止条件。
- 计算结果:根据状态转移方程和边界条件,计算出原问题的解。
动态规划的关键优势在于它能够通过记忆化(memoization)或表格化(tabulation)来避免重复计算相同的子问题,从而提高算法的效率。记忆化是指在递归过程中存储子问题的解,而表格化则是在迭代过程中使用表格来存储所有子问题的解。这两种方法都可以有效减少计算量,从指数时间复杂度降低到多项式时间复杂度。
理解和建议:
- 理解:在解决这类问题时,关键是识别出状态转移的规律,并能够将问题分解为更小的子问题。
- 建议:对于入门同学,建议从简单的动态规划问题开始,逐步理解状态转移的概念,并通过画图来帮助理解问题。
学习计划
制定刷题计划:
- 每日刷题:每天至少解决一到两个问题,保持连续性。
- 难度递增:从易到难,逐步增加题目的难度。
- 定期复习:每周回顾本周解决的问题,巩固知识点。
利用错题进行针对性学习:
- 记录错题:将做错的题目记录下来,分析错误原因。
- 针对性练习:针对错误原因,寻找类似的题目进行练习。
工具运用
结合AI刷题功能:
- 智能推荐:利用AI推荐系统,找到适合自己的题目。
- 错题分析:使用AI分析错题,找出薄弱环节。
与其他学习资源结合:
- 在线课程:结合在线课程,系统学习算法和数据结构。
- 书籍:阅读经典算法书籍,深入理解算法原理。
通过这样的学习方法和工具运用,可以更高效地利用豆包MarsCode AI刷题功能,提高学习效果。