靠山吃山,当然是借助编程了。
我家的孩子上小学三年级,比较喜欢数学,课外在深圳上学而思的创新预备班。去年寒假开始我教他学习C语言编程,每天1个小时左右,说是教,其实大部分时间是他自己看大部头的《C Primer Plus》,也算是半自学。每天在我给他的旧13寸MacBook Pro上用VS Code敲入书上的代码,或者自己改写,然后在Mac的终端下gcc编译,测试,倒也自得其乐。
也许有人会问,为啥小学生学编程,不学简单的Python而学更低层、更难的C语言呢?
原因之一是我自己最擅长C/C++,十八般兵器趁手就好,而且C语言并不难。原因之二是Python学校以后很可能会教,迟点再学也不晚。
言归正传。如今抗疫年代,深圳的小学推迟开学,数学老师每天在微信群布置几道题目,让同学们做好上传。昨天的这道题颇有意思,而且还能编程来验证,我和孩子深入探讨了一番,父子俩乐在其中。
原题目:
长度为60厘米的木头,小明每隔2厘米做一个标记,小琪每隔3厘米做一个标记,然后在标记处锯开,共有多少段木头?
老师认为是植树问题,我们分析后将其看成周期问题,每到6厘米处小明和小琪的标志会重叠,所以6厘米为一个周期。故:
解答:每 2 × 3 = 6厘米 为一个周期,这个周期有4段木头,共有:(60 ÷ 6) × 4 = 40 (段)。
是不是颇为简单?我和孩子又进一步探讨,如果将题目通用化,会怎么样?比如每3cm,每4cm或每5cm,9cm做记号,结果又会是几段?
我们将题目改为通用形式:
长度为M厘米的木头,小明每隔a厘米做一个标记,小琪每隔b厘米做一个标记,然后在标记处锯开,共有多少段木头?
首先还是得找出周期,周期显然是a和b的最小公倍数c,如果a、b没有公约数,则周期直接为a×b。
然后是找出周期内的段数,显然是:u = (c÷a) + (c÷b) - 1, 之所以减1,是因为在周期的最后a、b的标志重叠了。
总段数:x = (M ÷ c) ∙ u
还需考虑M不能整除c的情况,此时要加上余下的部分段数。
算法找到了,我们就来编程实现吧,结对编程,爸爸敲入代码,儿子在旁边看,理解代码并指出某些小错误。
第一步,先实现一个功能函数来可视化标记、并循环统计有多少段。当你用数学方法算出来后,可以用这个函数来验证。
这个函数相当于你自己用原始方法在纸上画线并做标记,然后数有几段,只是计算机不知疲倦,也不会出错。
// 实际验证: 打印并标志出线段
int Verification(int M, int a, int b)
{
int count = 0;
bool a_flag = false; // 是否该标志a了
bool b_flag = false; // 是否该标志b了
for (int i = 1; i <= M; i++) {
printf("-"); // 打印一短横线,表示一个单位长度
a_flag = false;
b_flag = false;
if (i%a == 0) { // a长度到了,需要给a标志
a_flag = true;
}
if (i%b == 0) { // b长度到了,需要给b标志
b_flag = true;
}
if (a_flag && b_flag) { // a、b标志重叠
printf("Y"); // 打印一个 Y
count++;
}
else if (a_flag) { // 仅仅a标志
printf("|"); // 打印一个竖线:|
count++;
}
else if (b_flag) { // 仅仅b标志
printf("v"); // 打印一个:v
count++;
}
}
if (!a_flag && !b_flag) { // 如果最后不是标志为a或b,意味着还有剩余,线段数加1
count++;
}
printf("\n\nline-segments count: %d\n", count); // 打印出实际测量出的线段数
return(count);
}
输出短横线-表示单位长度,竖线|为a的标志,v是b的标志,Y是a、b重叠处的标志。
假设M=60,a=2,b=3,这个函数可以输出这样的效果:
--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y
line-segments count: 40
很是直观。
第二步,实现一个求最小公倍数的函数
2和3的最小公倍数是两者相乘等于6,但如果4和6,则最小公倍数不是24而是12,在数学上计算最小公倍数需要分解素数,太复杂了。本算法简化之,采用蛮力运算。
// 计算最小公倍数
int LowestCommonMultiple(int a, int b)
{
int m = a>b ? a : b; // 取a b中的最大者为搜索起点
int n = a * b; // 两数相乘为最大的公倍数
for (int i = m; i <= n; i++) {
// 找到第一个可以同时被a或b整除的即为最小公倍数
if (i % a == 0 && i % b == 0) {
return(i);
}
}
return(n);
}
第三步,实现主函数,将数学算法实现并验证之
代码会说话,请看代码:
int main(int argc, char* argv[])
{
if (argc < 4) {
printf("Usage: %s M a b\n", argv[0]);
return 0;
}
int M = atoi(argv[1]);
int a = atoi(argv[2]);
int b = atoi(argv[3]);
if (a == 0 || b == 0 || M == 0) {
printf("M a b must > 0\n");
return 0;
}
// 数学算法开始:
int x = 0; // seegments number,存放最终结果的变量
if (a % b == 0 | b % a == 0) { // 如果a是b的倍数,则取其小者简单计算即可
int m = a > b ? b : a;
x = M / m;
if (M % m)
x++;
} else {
// 计算周期长度,等于a b最小公倍数
// 一开始使用a b相乘,不对。比如a=6, b=4, ab=24,实际的最小公倍数为12
// int cycle = a * b;
// 后来专门写了个函数来计算最小公倍数:
int cycle = LowestCommonMultiple(a, b);
printf("cycle = %d\n", cycle);
// 计算周期内的段数,减1是因为在一个周期结束时两者重叠,需去掉1个:
int unit = cycle / a + cycle / b - 1;
printf("unit = %d\n", unit);
x = (M / cycle) * unit;
// 计算余下部分的长度:
int y = M % cycle; //如果不能整除,y为余数
if (y > 0) {
int z = y / a + y / b;
if (y % a && y % b) { // 两者皆不能整除,则加一
z++;
}
x += z;
}
}
printf("M=%d, a=%d, b=%d, Segments Number: %d\n\n", M, a, b, x);
// 验证:
// verification for draw line
Verification(M, a, b);
return 0;
}
最后一步,编译,测试输出结果:
编译:gcc linesegment.cpp -o linesegment
测试1:
./linesegment 60 2 3
cycle = 6
unit = 4
M=60, a=2, b=3, Segments Number: 40
--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y--|-v-|--Y
line-segments count: 40
测试2:
./linesegment 60 4 6
cycle = 12
unit = 4
M=60, a=4, b=6, Segments Number: 20
----|--v--|----Y----|--v--|----Y----|--v--|----Y----|--v--|----Y----|--v--|----Y
line-segments count: 20
测试3:考虑除不尽的情况
./linesegment 65 3 7
cycle = 21
unit = 9
M=65, a=3, b=7, Segments Number: 28
---|---|-v--|---|--v-|---|---Y---|---|-v--|---|--v-|---|---Y---|---|-v--|---|--v-|---|---Y--
line-segments count: 28
不必羡慕生子当如孙仲谋,王健林的儿子会做2亿元的大买卖;咱们老鼠的儿子会打洞,程序员的儿子会写代码也不错。再说了,以后他当个科学家了,也得会编程验证自己的科学结果不是?