兴趣是最好的老师——爱因斯坦
1
最近中国在罗马尼亚大师赛的滑坡,让不少公众对中国最近的禁奥令口诛笔伐,认为是禁奥令直接导致了国际比赛奥数成绩的下滑。但要知道,两年前我国还处在全民奥数的癫狂中,这批参加赛事的孩子可都是在全民奥数的狂热氛围中脱颖而出的,要说和禁奥令有什么直接的因果关系,未免太牵强了。
然而,奥数禁了,家长们的焦虑感并未减轻,总得有某种方式证明自己的孩子比其他孩子优秀,以便让自己的孩子在小升初时占据有利位置。正好,国家提倡人工智能的做法与一些人的需求不谋而合,于是,我们看到了各种各样的机器人、编程课如雨后春笋般迅速地填补了奥数的缺口。
我今天不讨论孩子是否适合学奥数,是否适合学编程。仅仅从师资的角度,现在这一全民编程的趋势比奥数更值得担忧。不管怎样,家长至少都还懂一点小学数学,因此也能够对教学效果有一点点基本的判断;而对机器人和编程,懂行的家长要少的多,因此阿猫阿狗都可以来教编程。要知道,社会上对码农的需求还是很旺的,好的码农能拿到的薪水也是丰厚的。因此,从事少儿编程教学的师资水平的参差不齐程度恐怕要比奥数更甚,建议家长给孩子报编程课尤其要有一双火眼金睛。
2
自从学了点Scratch后,昍从去年暑假就嚷嚷着要学C++,我一直干耗着不让他学,这一晃就整整吊了他半年多的胃口,最近终于允许他开始学一点。结果就是,孩子的学习兴趣高涨,动力十足。
其实,虽然没学编程,这半年他一点没闲着,空闲时间整天折腾自己的电脑,从运行病毒把机器搞崩,到装虚拟机在虚拟机里试毒,再到安装双系统,一切都是他自己搞定,甚至整天听的音乐都是熊猫烧香背景音乐。我所能做的就只有三点:一是偶尔告诉他编程能干什么,比如让他着迷的病毒就是一行行枯燥的代码构成的;二是告诉他什么不能做,比如通过email给人发病毒文件是不能做的;三是严格约束他沉迷于电脑的时间,一方面是保护视力,另一方面也是一种饥饿营销。
有人说现在的孩子太苦了,上那么多的课。我不这么认为,很多课是家长强加给孩子的,而且带着极强的功利性。孩子上课外兴趣班,本应能够让孩子发现自己的兴趣,但有些课外班则适得其反,孩子学完反而没兴趣了。昍二年级的时候本来对围棋有点兴趣,于是给他报了个围棋班,但上了一个学期后,他反而没有兴趣了。从此,我后来就没给他上任何兴趣班。有人担心孩子课后不上班是不是就会虚度时间,这可能多虑了。只要控制好游戏和电视,孩子空闲的时间会去真正做一些自己感兴趣的事情,虽然节奏可能会慢一点。给孩子空间和时间,孩子会比我们那时候有更多机会去真正寻找自己真正感兴趣的方向。回想我自己,之前连计算机都没碰过,最后稀里糊涂就选了计算机专业作为自己一辈子的职业,理由只是听闻计算机热。而到最后,也没有当码农,而是follow了自己小学时候的理想:当一名教师。
之前,我一直担心四年级孩子学C++这样的编程语言是不是太早了。经过半个月,我的疑虑一点点地消除了。不仅如此,我发现昍在编程上的潜力可能要超过他在数学上的潜力。昍妈戏说,那是因为我是计算机博士,而不是数学博士。不管怎样,我发现用适当的方法去学点编程,不仅可以对比编程思维和数学思维的差异,还可以让孩子加深对数学与计算的理解。
3
有人说数学好编程就好,也有人说编程好数学也差不了。没错,两者有紧密关联,相辅相成,但也有一定的区别。下面是最近和昍一起讨论过的编程书上的几个例子,都是从数学和编程两个角度来引导他思考,也算是一种别样的尝试。
例1:求1+2+3+4+…+100的和。
数学做法:基本就是等差数列求和,所以是(1+100)×100/2=5050,如果像高斯小时候的同班同学那样死算,势必会被当成傻瓜。
编程做法:我相信包括绝大多数计算机博士在内的都会用下面的程序
int sum = 0;
for(int i=1; i<=100; i++)
sum += i;
这无疑就是高斯同班同学的做法,但为什么我们不嘲笑这个程序?因为我们通常认为计算机算的非常快,我们需要做的就是告诉它一个明确的计算规则就可以了。
例2:请给出斐波那契数列1,1,2,3,…的第100项。
编程做法:循环+迭代
int a = 1, b = 1, c;
int i = 3;
while(i<=100){
c=a+b;
a = b;
b = c;
}
孩子需要花一点时间理解迭代的做法。实际上,和之前的求和类似,这也是一种穷举和递推的做法。我们知道计算的递推规则an+2=an+1+an, 为了计算第100项,我们得把前面的每一项都计算出来。
数学做法:数学家则远远不满足对这个计算规则的确定,还希望有一个通用的公式能够直接求出任何指定的一项,因此才有了斐波那契数列的通项公式。也正因如此,我们看到了那个著名的黄金分割数。一个整数序列的通项公式,竟然和一个无理数联系起来了。
例3:写出下面程序的输出
int main() {
int i, j;
for(i=20, j=0; i<=50; i++, j=j+5)
if(i==j)count<
return 0;
}
编程做法:看程序写输出一般都是人脑逐步模拟程序的执行,然后从有限步执行推导出程序的功能,写出输出。
这里,i从20开始,j从0开始,i每次增加1, j每次增加5,当i和j相等时输出i的值。因此,执行了5次循环判断后,输出i=25。
数学做法:如果我们把这个问题按数学的思维来解读一下,j在起点,i在j前面20米,j开始追i,j每秒走5米,i每秒走1米,请问j追上i时离起点多远?
这就成了一个简单的追及问题,j花20÷(5-1)=5秒追上i,此时距离起点25米。
在这里,大家可以看到,数学可以做转化和建模,把一个问题转化和建模为另一个熟知的问题。
例4: 在大学校园里,由于校区很大,没有自行车上课办事会很不方便。但实际上,并非去办任何事情都是骑车快,因为骑车总要找车、开锁、停车、锁车等,这要耽误一些时间。假设找到自行车、开锁并骑上自行车的时间为27秒,停车锁车的时间为23秒,步行每秒行走1.2米,骑车每秒行走3.0米。输入距离(单位:米),输出是骑车快还是走路快。
编程做法:从编程的角度,任意输入一个距离,那么我们可以分别算出骑车的时间和步行的时间,然后比较一下就可以得出答案。程序基本如下:
int d;
cin>>d;
double twalk = 50+d/3.0;
double tride = d/1.2;
if(twalk
cout<< “走路快”<
else if(twalk>tride)
cout<<”骑车快”<
else
cout<<”一样快”<
编程思维就和我们大部分人考虑问题的方式差不多,直肠子,要什么,就求什么。当然,上面的程序之所以说是基本上是这样,是因为还有点小问题。问题在于计算机本身的限制:受制于表示的位数限制,计算机不能精确地存储一个高精度的小数,例如一个无限循环小数或无理数,那么当两个数非常接近时可能会出错。
一个改进的做法是,尽量不做除法。我们可以把twalk和tride两边都乘以6,得到:
twalk6 = 300+2*d;
tride6 = d*5;
此时,再去比较twalk6和tride6会好的多。
数学做法:首先分析出一定存在一个临界值x,当距离超过这个临界值时骑车快,而小于这个临界值时是走路快,如果恰好是这个临界值x,那么两者所花时间相等。
可以这么来解读这个题:走路速度每秒1.2米,先走了50秒,然后骑车人开始追走路的人,骑车速度每秒3米,那么骑车人追上走路人时,骑了多少路?
解这个题,走路先走了60米,骑车人从开始到追上花了60÷(3-1.2)=100/3秒,总共骑了100/3×3=100米。也就是说距离是100米,那么走路和跑步一样快,超过100米,骑车快,少于100米则是步行快。
可以看到,这里我们又一次把它建模成了一个追及问题。
例5:为了学生的卫生安全,学校给每个住宿生配一个水杯,每只水杯3元,大洋商城打八八折,百汇商厦“买八送一“。输入学校想买水杯的数量,请你当”参谋“,算一算:到哪家购买较合算?输出商家名称。
编程方法:还是直肠子,任意输入水杯的数量,我们可以先计算出到每个商家购买的总费用,然后比较输出:
int cups;
double total;
cin>>cups;
a = cups*3*0.88;
b = (cups – cups/9)*3;
if(a
cout<<”大洋商城”<
else
cout<<”百汇商厦”<
数学做法:简单分析一下,买八送一最划算的就是买9的倍数的杯子数,此时能达到最优的折扣,是88.9%。这个最优折扣都比大洋商城来得高,因此不用算,无论买多少个杯子,都是大洋商城划算。
这个小例子体现了数学和工程技术思维的差别。我们投稿一些计算机会议或期刊,即便做了理论分析与论证,有些审稿人也要求做实验验证。隔壁办公室是个做密码学的老师,他们的论文就完全不同,都是证明完了直接结束。实际上确实,数学证明是最严谨的,实验验证还受很多环境的影响,可信度才要打问号。
例6:鸡兔同笼,共有头35个,腿90条,问鸡兔各有几只?
数学解法:五花八门的解法很多,如抬腿法,假设法,方程法等等。
比如假设法,可以假设全是鸡,那么一共有70条腿,比实际少了20条腿,为什么?(能够反问自己为什么是我认为最重要的一种素质)。因为把兔子看成是鸡了。一只兔子假设成一只鸡少2条腿,所以兔子是(90-70)÷2=10只,鸡是25只。
也可以用方程,假设有x只鸡,那么兔子有35-x只,所以得到方程2×x+4×(35-x)=90,解得x=25。
编程做法:编程可以很暴力。既然共有35个头,那么鸡最少0只,最多35只,枚举一遍逐个验算即可。
for(int i=0;i<=35;i++)
if(i*2+(35-i)*4==90)
cout << “鸡=”<“,兔子=”<<35-i<
可以看到,编程做法更类似于我们数学中的验算。本质上是用方程建模,用枚举求解。
例7:求1+(1+2)+(1+2+3)+…+(1+2+3+…+10)的值
编程做法:我的第一个想法是用个双重循环,没想到昍第一个想法居然是用下面的一重循环。很明显,这比双重循环更简练。这实际上让计算机少干了不少活,用计算机的术语说,就是计算复杂度(另一个是要考虑的是空间复杂度)降低了。
int i, a=0, s=0;
for(i=1;i<=10;i++)
{
a=a+i;
s=s+a;
}
cout<
数学做法:按数学思维,一开始就要把这个问题抽象化为1+(1+2)+(1+2+3)+…+(1+2+3+…+n)。一种做法是变成1×n+2×(n-1)+3×(n-2)+…+n(n-(n-1))=1×n+2×n+…+n×n-(1×2+2×3+…+(n-1)×n),后面就不介绍,学过等差和列项应该有所了解。
0
推荐