最大乘法算式-第13届蓝桥杯选拔赛Python真题精选

文摘   教育   2024-06-04 15:00   湖北  
[导读]:超平老师的Scratch蓝桥杯真题解读系列在推出之后,受到了广大老师和家长的好评,非常感谢各位的认可和爱。作为回馈,超平老师计划推出《Python蓝桥杯真题解析100讲》,这是解读系列的第80讲。

最大乘法算式,本题是2022年3月13日举办的第13届蓝桥杯青少组Python编程选拔赛真题编程部分第5题。定一个正整数M和一个数字字符串,使用M个乘号插入到字符串中,请计算并输出最大乘积。

先来看看题目的要求吧。

01
题目说明 

编程实现:

给定一个正整数 M(1 ≤ M ≤ 5)和一个只包含数字的字符串(5 < 字符串长度 ≤ 20)。使用M个乘号插入到字符串中,且两个乘号不能相邻,插入后生成一个乘法算式。找出一种使乘法算式数值最大的插入方式,并将结果输出。(乘号不能放在字符串的首尾位置)

如M = 2,字符串为123456,插入2个乘号。插入方式有:

1 * 2 * 3456 = 6912 , 1 * 23 * 456 = 10488 , 1 * 234 * 56 = 13104 , 1 * 2345 * 6 = 14070 ,12 * 3 * 456 = 16416 , 12 * 34 * 56 = 22848 , 12 * 345 * 6 = 24840 , 123 * 4 * 56 = 27552 ,123 * 45 * 6 = 33210,1234 * 5 * 6 = 37020,

其中乘法算式数值最大是第十种,为37020。

输入描述:

第一行输入一个正整数 M(1 ≤ M ≤ 5),表示乘号个数 

第二行输入一个只包含数字的字符串(5 < 字符串长度 ≤ 20),表示要插入M个乘号的字符串

输出描述:

输出一个整数,表示最大乘积数值

样例输入:sd2123456

样例输出:

37020
02
思路分析 

这是一道经典的算法题,涉及的知识点包括循环、列表、字符串、枚举算法和动态规划等。

实际上,这是信息奥赛的一道原题,出自2000年NOIP提高组的第二题,原题是这样描述的:

今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰 90 周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友 XZ 也有幸得以参加。

活动中,主持人给所有参加活动的选手出了这样一道题目:设有一个长度为N的数字串,要求选手使用 K个乘号将它分成K + 1个部分,找出一种分法,使得这K+1个部分的乘积能够为最大。

虽然描述有所不同,但意思完全相同,题目还是非常有难度的。
通用的解决方案是使用动态规划,为了让大家更好的理解这个过程,超平老师准备使用3种算法来分析和解读,分别是:
  • 枚举算法
  • 递归算法
  • 动态规划算法
1. 枚举算法
对于编程中出现的大部分算法问题,都可以使用枚举算法来解决,只是效率相对不高。
以题目中的样例数据为例,在字符串"12345"中插入2个乘号,最基础的方法就是将所有的情况列举出来。
这是典型的组合问题,就是在5个位置中任选两个,我们可以使用数学中的”打枪法“,固定一个位置,变换另一个位置。
当第一个✖️在1和2之间时,第二个✖️有4个位置可选,如图:

当第一个✖️在2和3之间时,第二个✖️有3个位置可选,如图:

当第一个✖️在3和4之间时,第二个✖️有2个位置可选,如图:

当第一个✖️在4和5之间时,第二个✖️有1个位置可选,如图:

一共有10种组合,将这10种组合的乘积计算出来,就可以找到最大值37020了。
对于组合问题,在Python编程中,我们可以直接使用combinations()函数,5种组合中任选两个位置,可以使用如下代码:
combinations(range(5), 2)
就可以得到如下10种组合:
(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)
对于每一种组合,对字符串"123456"进行截取运算,再转成整数,相乘即可。
比如对于组合(0,1),将字符串"123456"拆分成3段,分别为"1"、"2"、"3456",转成整数相乘如下:
1 * 2 * 3456 = 6912
按照同样的方式计算出所有组合的乘积,就可以找到最大值了。
2. 递归算法
枚举算法理解起来比较简单,但是随着字符串长度的增加,时间复杂度会急剧增加,算法效率大大降低,会出现超时情况。
接下来,我们使用递归算法来分析和解决,仍然以字符串s = "123456",M = 2为例进行分析和说明。
首先,我们需要给递归函数下一个明确的定义,这里有两个变量,一个是字符串的长度,一个是乘号的个数。
因此,可以定义f(i, j),表示字符串前i个数字使用j个乘号能够得到的最大乘积。
递归算法的核心思想就是找到f(n)f(n - 1)之间的关系,也就是最后一步是如何演变过来的。
对于这里定义的f(i, j)来说,需要分析f(i ,j - 1)到f(i ,j)这一步是如何计算的,也就是第前6个数字串中已经有一个✖️了,第二个✖️放在哪儿,可以得到最大值的问题。
由于第二个✖️的位置有多种选择,因此,我们需要分情况讨论。
当第二个✖️在5和6之间时,它表示的是前5个数字串中插入一个✖️的情况,即f(5, 1),如图:
第二个✖️在4和5之间时,它表示的是前4个数字串中插入一个✖️的情况,即f(4, 1),如图:如图:

第二个✖️在3和4之间时,它表示的是前3个数字串中插入一个✖️的情况,即f(3, 1),如图:如图:

第二个✖️在2和3之间时,它表示的是前2个数字串中插入一个✖️的情况,即f(2, 1),如图:如图:

发现这其中的规律了吗?
对于f(6, 2)而言,我们需要分别考虑如下4种情况:
f(5, 1) * s[5: 6]f(4, 1) * s[4: 6]f(3, 1) * s[3 :6]f(2, 1) * s[2: 6]
然后从这几种情况中,找到最大值即可。
因此,对应f(i, j)而言,其推导公式可以描述如下:
for k in range(1, i):  f(i, j) = max(f(i, j),f(k, j - 1) * int(s[k:i]))
递归是需要结束条件的,这里的结束条件有两个:
1). j = 0,即没有✖️时,就等于数字串本身,即f(i, j) = int(s[: i]);
2). i <= j时,没办法插入所有✖️,直接返回0。
普通的递归会存在大量的重复计算过程,因此,可以增加一个备忘录,将每一次的计算结果保存起来,从而提升算法效率。
3. 动态规划算法
如果说递归是自顶向下的推导过程,那么动态规划就是自底向上的推导过程,它们都有着共同的推导公式。
使用动态规划算法的要点有如下4个:
  • 定义DP数组
  • 初始化DP数组
  • 状态转移方程
  • 遍历顺序
1). 定义DP数组
根据前面的分析,DP数组可以描述如下:dp[i][j]表示字符串前i个数字中插入j个✖️的最大乘积。
这是一个典型的二维DP数组,对于字符串s = "123456",M = 2来说,我们要计算的是dp[6][2],如图:

所谓的动态规划,其实就是逐步填充表格的过程。
2). 初始化DP数组
根据前面的分析,初始化有两种情况:
  • i <= j时,dp[i][j] = 0;
  • j = 0时,dp[i][j] = int(s[:i]);
对应的二维表格如图所示:

实际上,可以将所有空白单元格中的值初始化为0,后续更新即可。
3). 状态转移方程
和递归算法中的推导公式是一样的,如下:
for k in range(1, i):  dp(i, j) = max(dp(i, j),dp(k, j - 1) * int(s[k:i]))
这里就不再赘述了
4). 遍历顺序
遍历顺序很重要,我们逐一来分析。
先从dp[2][1]开始,它表示前2个数字中插入一个✖️的最大乘积,根据上面的状态转移方程,k只能取1,因此:
dp[2][1] = max(dp[2][1], dp[1][1] * int(s[1:2]))= max(0, 1 * 2)= 2
对应的dp表格如下:

接着是dp[3][1],它表示前3个数字中插入一个✖️的最大乘积,k有两个取值,分别是1和2,需要根据dp[1][0]和dp[2][0]来计算,如下:
dp[1][0* int(s[1:3]) = 1 * 23 = 23dp[2][0] * int(s[2:3]) = 12 * 3 = 36
因此,dp[3][1] = 36,如图:

再来计算dp[4][1],表示前4个数字中插入一个✖️的最大乘积,k有3个取值,分别是1、2和3,需要根据dp[1][0]、dp[2][0]和dp[3][0]来计算,如下:
dp[1][0* int(s[1:4]) = 1 * 234 = 234dp[2][0* int(s[2:4]) = 12 * 34 = 408dp[3][0* int(s[3:4]) = 12 * 34 = 492
因此,dp[4][1] = 492,如图:

依此类推,可以分别计算dp[5][1]和dp[6][1],结果如下:

同理,计算第3列中的所有单元格,得到最终的二维表格数据,如下:

通过上面的分析过程,相信你已经明白了,这一次,我们要采取先遍历列再变量行的策略,即从左到右,从上到下。

思路有了,接下来,我们就进入具体的编程实现环节

03
编程实现 
根据上面的思路分析,我们使用三种方法来编写程序:
  • 枚举算法

  • 递归算法

  • 动态规划

1. 枚举算法

根据前面的思路分析,我们使用combinations()函数获取所有组合,逐个计算乘积,从而获取最大值,代码如下:

代码比较长,说明4点:

1). 首先使用组合函数,获取所有组合,然后对字符串截取,将截取的数字串保存到临时列表temp中,如['1', '2', '3456'];

2). 为方便计算数字串的乘积,定义了一个f()函数,它将temp列表中的数字串依次取出,转成整型,累乘得到成绩,保存到res列表中;

3). res列表保存的是所有乘法算式的乘积,最后使用max()函数获取最大值并输出;

4). 在拆分字符串的时候,需要注意不断更新和计算起点start和end,同时不要忘了最后一个乘号✖️后面的数字串。

2. 递归算法

使用带备忘录的递归算法,编写代码如下:

代码不算少,说明两点:

1). 为了方便,这里将备忘录列表作为递归的参数进行传递,真正的关键还是i和j这两个参数;

2). 在循环取值的时候,k是从1开始的,确保函数能满足结束条件;

2. 动态规划

根据前面的思路分析,使用动态规划算法,编写代码如下:

代码相对较少,强调两点:

1). 在遍历二维列表的时候,我们始终要遵循i表示行,j表示列的原则,如果要先遍历列,那就先写j,再嵌套i;

2). 对于每一列,起点是从 i + 1开始的,请参考填充DP表格的过程。

至此,整个程序就全部完成了,你可以输入不同的数据来测试效果啦。

04
总结与思考 
本题代码在12 ~ 20行左右,涉及到的知识点包括:
  • 循环语句;

  • 列表操作;

  • 枚举算法;

  • 组合函数;

  • 递归算法;

  • 动态规划算法;

作为本次测评的最后一题,难度较大。关键点有两个,一是使用组合函数快速解决问题,二是使用递归思维深入分析计算最大乘积的过程,找到其中的规律,然后选择相应的算法编程实现。

对于本题而言,最基础的方法就是枚举算法,最高效的方法当属动态规划,而递归则是一种讨巧的实现方法。

枚举算法简单粗暴,如果一时半会找不到其他的解决方案,建议可以从枚举算法着手,先确保能解决问题或部分解决问题。

在使用枚举算法解决问题的过程中,说不定灵感就来了,想到更好的方法。实际上,大部分算法都是在枚举的基础上进行优化的。

很多同学都喜欢动态规划,从而忽略了递归算法,我倒是提倡大家多尝试使用递归思维来分析解决问题。

从本质上讲,动态规划和递归算法的核心是一样,但是递归算法的编写难度要小于动态规划,往往会给你带来意想不到的效果。

最后说说动态规划,动态规划的分析过程就是填充表格的过程,所以在解决动态规划问题时,一定要踏踏实实的逐步分析并填充表格,最终的代码往往是比较简单的。

你还有什么好的想法和创意吗,也非常欢迎和超平老师分享探讨。

如果你觉得文章对你有帮助,别忘了点赞和转发,予人玫瑰,手有余香😄

需要源码的,可以添加本人微信

另外,超平老师创建了一个蓝桥杯备考交流群,这是专门为老师和家长打造的免费社群,您可以与来自全国各地的老师、家长共同交流经验,分享学习心得。

超平老师也会给大家带来及时的赛事动态,备考攻略,真题资源分享,帮助各位更好备考第15届蓝桥杯赛事,力争取得优异的成绩。

扫码或长按加入微信群

超平的编程课
青少儿编程教育专家,中国人民大学硕士,大学讲师,曾任知名上市机构金牌讲师,16年编程教研经验。大耳猴少儿编程联合创始人,致力于通过编程教育提升孩子的逻辑思维、数学思维和计算思维,迎接AI时代。
 最新文章