程序设计中两个典型问题的关联与启示

2022-05-30 15:43焦华
电脑知识与技术 2022年10期
关键词:认知程序设计大数据

摘要:各种高级语言程序设计类教程通常都会涉及两类问题的编程——棋盘麦粒计数问题和汉诺塔金片移动问题,但并没有发现、更没有证明两者之间的关联性,也没有深入到启示层面。在文章中不仅给出了答案,而且还具体到课程教学中如何设置提问、如何引导学生思考,达到思维训练和创新能力培养的目的。由此可见对编程类人才培养有参考价值。

关键词:程序设计;教学;思维;认知;大数据

中图分类号:TP311      文献标识码:A

文章编号:1009-3044(2022)10-0104-03

1 引言

当今的世界是处于大数据和人工智能的时代,迫切需要掌握算法及编程的技术类人才,高等学校如何面向市场需求、向社会输送合格学生是一个重要的研究课题。目前这方面的研究文章很多,但大多数局限于抽象的讨论,具体应用于课堂的价值不大。作为直面学生的一线老师,更需要落到实处的解决方案,此文从两个典型问题出发,较深入细致地探究课堂教学及人才培养,希望能达到“抛砖引玉”的效果。

2 典型问题之一——棋盘麦粒的计数

有一个古老的印度传说:舍罕王要奖赏宰相达依尔——国际象棋的发明人。国王询问他想要什么样的奖赏?他回答说:“陛下在这张棋盘的小格子放一些麦子,第1格放1粒麦子,第2格放2粒,第3格放4粒,总之,后面格子里麦粒数量总是前面格子里的2倍。这样摆满了64个格子的棋盘上所有的麦粒,就是您对仆人的奖赏!”

国王感到这要求实在太简单了,马上令人扛来麦子,但很快就不够用了。于是人们将一袋袋麦子搬来计数时,大家才发现:就算将整个印度的麦粒都取来,也达不到宰相的要求。

宰相到底要多少麦粒?若体积 1 立方米的麦粒大约有 [1.42×108]颗,问宰相要求的麦子为多少立方米[1]?

依据题意,编写C语言程序(qpmn1.c)如下:

#include "stdio.h"

#include <stdlib.h>

int main()

{double t;           //t表示麦粒体积

double s = 0;       //s表示麦粒总数

double n = 1;       //n表示小格子麦粒颗数

int j;

for(j=1; j<=64; j++)

{s=s+n;        //各格子麦粒累加

n=n*2;        //小格子麦粒数

}t=s/(1.42*100000000);

printf("s=%.lf颗麦粒\n",s);   //麦粒的颗数

printf("t=%.lf立方米麦粒\n",t);   //麦粒的体积

system("pause");

return 0;

}

运行得到:s=18446744073709552000颗麦粒,t=129906648406立方米麦粒。这是两个非常庞大的数量,按当时的生产力估算:用两千年的时间,全世界也难以生产出宰相要的这么多麦子[1]。

注意到这个问题本质是要计算[20+21+22+23+...+262+263]的值,由等比数列的前n项和公式得:[20+21+22+23+...+262+263=264-1],下面编程计算[264-1]的值(由新算法产生新程序) ,得到简化的C程序代码(qpmn2.c)如下[2]:

#include "stdio.h"

#include <math.h>

int main()

{int x = 2,y,i;

double result;

printf("input y:");

scanf("%d",&y);

for(i=1;i<=y;i++)

{result = pow(x, i);

result=result-1;

printf("%.lf  ",result);

if(i%4==0) printf("\n");

}getch();

return 0;

}

程序運行结果如下:

运行结果的64个数据是[21-1],[22-1],[23-1],...[263-1],[264-1]的计算结果。之所以这样编程是为了进行数据比对,看到数据的变化过程,[2i-1(i=1,2...64)]表示的是前面i个棋盘格子的麦粒总数。在C语言的教学过程中,要引导学生发现问题和解决问题,培养学生的独立思考能力和创新能力。在此提问:运行结果有错误吗?如果有错,错在什么地方呢?64个数据[2i-1(i=1,2...64)]应该都是奇数,但最后的11个数据却是偶数,所以一定是错的。在程序中用result存储运算结果,定义为双精度实型(double result;) ,而double型数据只能保证15位有效数字,千万不要认为电脑输出的数字都是绝对精确有效的。若修改为无符号双长整型(unsigned long long result;) 程序代码(qpmn3.c)如下[2]:

#include "stdio.h"

#include <math.h>

int main()

{int x = 2,y,i;

unsigned long long result;

printf("input y:");

scanf("%d",&y);

for(i=1;i<=y;i++)

{result = pow(x, i);

result=result-1;

printf("%lld  ",result);

if(i%4==0) printf("\n");

}

getch();

return 0;

}

运行结果如下:

注意到最后的11个数据有10个已修改正确,但最后1个发生了数据溢出错误[3]。

验证一下:用Windows自带计算器选科学型可得[264-1=]18446744073709551615,而程序运行结果是18446744073709552000,两者有误差。

为什么用C语言计算庞大的数会产生错误?这个问题作为作业留给学生思考。

梳理一下思路:这个问题的计算或编程都是比较简单的,国王贸然答应宰相,是对数量缺少正确的认知,是对按几何增长的数量估算不足。常言道“不算不知道,一算吓一跳”,宰相戏弄了国王,国王有惩罚的手段吗?C语言课程教学中作为作业让学生思考,可在一个星期之后给出答案……答案是:国王要惩罚宰相必须抓住问题的关键点——麦粒数量庞大。

让宰相亲自到粮仓去数麦粒,假定1秒钟能数2粒麦子,每天数12小时,需要10年才能数出20立方米。要数完那么多麦粒将需要2900亿年……宰相能活多少年?再有每天数麦子的活法谁能承受?如此数下去人会疯掉……这类作业可拓展思维、启迪智慧[4]。

3 典型问题之二——汉诺塔金片的移动

汉诺塔问题——印度另一个古老的传说,大梵天开天辟地时做了3根宝石针,其中一根针上穿好了64片金片,金片大小不等,从下到上按由大到小的顺序。大梵天下指令让婆罗门按照以下法则移动金片:一次只能移走一片,无论在哪根针上,小片要在大片的上方。3根宝石针分别用A、B、C做标记,初始状态是A针上有64片金片,目标是将A针上的金片借助于B针全部移到C针上,请找出移动金片的步骤。

用C语言编程时要用到函数的递归调用,几乎所有的C语言教程都要写入这个典型实例,给出相关的程序代码,运行得到金片的移动步骤。下面的程序代码(hanoi_tower1.c) 有新颖的地方,对传统程序做出了改进,增加了标识和统计移动步骤的功能[5]。

#include <stdio.h>

int js=1;

int main()

{void hn(int n,char z1,char z2,char z3);

int m;

printf("Please input the num of gold diskes:");

scanf("%d",&m);

printf("The moving-step  %d gold diskes:\n",m);

hn(m,'A','B','C');

getch();

return 0;

}

void hn(int n,char z1,char z2,char z3)

{void mv(char x,char y);

if(n==1)

mv(z1,z3);

else

{hn(n-1,z1,z3,z2);

mv(z1,z3);

hn(n-1,z2,z1,z3);

}}

void mv(char x,char y)

{ printf("%d:",js);

printf("%c-->%c\n",x,y);

if(js % 10==0) getchar();

js++;

}

程序運行时5片金片的移动步骤如下:

Please input the num of gold diskes:5

The moving-step  5 gold diskes:

1:A-->C  2:A-->B  3:C-->B  4:A-->C  5:B-->A  6:B-->C  7:A-->C

8:A-->B  9:C-->B  10:C-->A  11:B-->A  12:C-->B  13:A-->C  14:A-->B

15:C-->B  16:A-->C  17:B-->A  18:B-->C  19:A-->C  20:B-->A  21:C-->B

22:C-->A  23:B-->A  24:B-->C  25:A-->C  26:A-->B  27:C-->B  28:A-->C

29:B-->A  30:B-->C  31:A-->C  (程序实际运行得到31行,太占篇幅,这里做了合并)

注意:5片金片的移动步骤前7次恰好就是3片金片的全部移动步骤,这不是偶然的而是必然的。也就是说n片金片的全部移动步骤是n+2片金片最前面的部分移动步骤(“隔代遗传”) ,为什么有这样的结论?在这里要设置课堂互动,让同学们思考后回答[6]。

若程序中删去if (js % 10==0) getchar();这一行,运行时若输入35到64之间的较大自然数时,屏幕不断向上翻滚显示移动步骤,一直停不下来,这是为什么呢?仔细思考一下,莫非是电脑已得到结果,但显示器的显示速度跟不上?64片金片至少需要移动多少次才能实现目标呢?这个经典问题的答案是至少需要移动18446744073709551615次。假如金片移动1次需要1秒,则共需18446744073709551615秒时间。平年有365天是31536000 秒,闰年有366天等于31622400秒,每年平均下来是31556952秒,计算结果是移完这些金片需要5845.54亿年以上(18446744073709551615秒) ,然而地球至今存在不超过45亿年,太阳系预期寿命科学家们预测也不过数百亿年。如若超过5845.54亿年,太阳系、银河系、地球上的生物、庙宇、梵塔等,都早已灰飞烟灭。

为了验证前面的解决方案是不是因为显示器的显示速度跟不上,修改程序不显示金片的移动步骤、只统计移动次数的功能(hanoi_tower2.c)  [6]。

#include <stdio.h>

double js;

int main()

{void hn(int n,char z1,char z2,char z3);

const t=32;

int m;

for(m=1;m<=t;m++)

{js=0;

hn(m,'A','B','C');

printf("%.lf  ",js);

if(m%4==0) printf("\n");

}getch();

return 0;

}void hn(int n,char z1,char z2,char z3)

{void mv(char x,char y);

if(n==1)

mv(z1,z3);

else

{hn(n-1,z1,z3,z2);

mv(z1,z3);

hn(n-1,z2,z1,z3);

}}

void mv(char x,char y)

{js=js+1;  //printf("%c-->%c\n",x,y);

}

程序運行结果如下:

1  3  7  15

31  63  127  255

511  1023  2047  4095

8191  16383  32767  65535

131071  262143  524287  1048575

2097151  4194303  8388607  16777215

33554431  67108863  134217727  268435455

536870911  1073741823  2147483647  4294967295

若将程序中语句const t=32;改成const t=64;运行时结果将一直出不来(递归需要时间) ,这表明电脑是边计算边输出,而不是电脑已得到结果、只是显示器的显示速度跟不上。千万不要小瞧这个结论,90%以上的计算机教师教了一辈子程序设计,仍然错误认为电脑已有答案,只是需要排队显示,数据量太大,显示需要时间等候。

4 两个典型问题的关联与启示

由前面的分析讨论可知:棋盘麦粒计数问题中,前面i个棋盘格子的麦粒总数为[2i-1(i=1,2...64)]。而汉诺塔金片的移动问题,从程序运行结果上看,i片金片至少需要移动的次数居然也是[2i-1(i=1,2...64)],多么神奇的巧合?不是巧合是规律,下面用数学归纳法证明:

当i=1时,移动1次,[20=21-1]

当i=2时,移动3次,[20+21=22-1]

当i=3时,移动7次,[20+21+22=23-1]

假设当i=k时,移动[20+21+...+2k-1=2k-1]次,那么当i=k+1,即A针上有k+1个盘子时,操作步骤如下:

将A针上面k个盘子借助C针移动到B针上,移动次数为[2k-1]次,之后将A针上最大盘子直接移动到C针上,移动次数为1次,最后将B针上面k个盘子借助A针移动到C针上,移动次数为[2k-1]次,总共移动次数为:

[(2k-1)+1+(2k-1)=2*2k-1=2k+1-1]

结论得证[7]。

启示1:“趋乐避苦”是人性,决定了人类对数量的认知往往容易犯错误。比如初次涉及棋盘麦粒计数问题的人,不会去计算宰相要求奖励的麦粒数量到底是多少(仅凭感觉认为并不多) ?全世界的小麦产量是多少?仔细计算才发现是非常庞大的数量。正确的选择基于正确的认知——对客观世界的正确认知、对人类社会的正确认知、对自身的正确认知。“认识你自己”点燃了古希腊文明火花,“知人者智,知己者明”凝聚了中国古老的智慧。与世界这个无穷大量([β→∞]) 相比,个人就是一个无穷小量([α→0]) 。

启示2:“知易行难”的新解释:无论是棋盘麦粒计数问题或是汉诺塔金片移动问题,计算出总量都不难,困难的是具体的“数麦粒”和具体的“移动金片”,因为具体步骤非常庞大。推广到个人对数量的认知,大家都知道世界有75亿人,中国14亿人,但对这总量的感性认识并不够,让个人痛苦地去数一数才会有体会。很多位高權重的人、很多富有的商人容易膨胀,他们对75亿分之一、14亿分之一有多小缺少正确的认知。从这个角度看,自大的人是多么的愚蠢。“强极则辱、情深不寿、慧极必伤”是古训,“谦谦君子、温润如玉”是处世之道。

启示3:过分强调过程考核的教学评估是有很大弊端的,因为这样会把教师变成“数麦粒”的人、变成“移动金片”的人。有些学校机械地、僵化地理解评估指标体系,实则偏离了顶层设计的宗旨。试想这样为完成繁杂指标体系、收集庞大支撑材料“挥汗如雨”的教师会有多少独立思考?又怎么能培养出有创新思维的学生?

5 结束语

课程思政是各门课程课堂教学中非常重要的组成部分,正确的“三观”培养、正确的认知引导是高校人才培养的基石。这两个典型的编程案例告诉我们:“千里之行,始于足下”“不忘初心,方得始终”。在“百年未有之大变局”的时代,青年学生必须努力前行,为中华民族实现伟大复兴贡献自己的一份力量!

参考文献:

[1] 谭浩强.C程序设计[M].5版.北京:清华大学出版社,2017.

[2] 梅创社,李培金.C语言程序设计[M].北京:北京理工大学出版社,2010.

[3] 焦华.基础编程的思考方法[J].软件,2018,39(3):57-62.

[4] 台海江,许鑫,郑光.《C语言程序设计》课程教学改革探讨[J].现代计算机(专业版),2018(33):74-77.

[5] 焦华.C编程教学导入线索分析[J].电脑知识与技术,2017,13(12):126-129,156.

[6] 焦华.C/C++编程中典型案例分析与思路拓展[J].电脑与信息技术,2017,25(3):36-39,56.

[7] 安光勇.以数学算法为基础的C程序编程技巧[J].电子测试,2017(7):76-77.

【通联编辑:谢媛媛】

收稿日期:2022-01-25

作者简介:焦华(1964—) ,男(苗族) ,贵州贵阳人,副教授,硕士,研究方向为算法与程序。

猜你喜欢
认知程序设计大数据
基于Visual Studio Code的C语言程序设计实践教学探索
从细节入手,谈PLC程序设计技巧
高职高专院校C语言程序设计教学改革探索
《红楼梦》隐喻认知研究综述
关注生成,激活学生认知
基于大数据背景下的智慧城市建设研究
PLC梯形图程序设计技巧及应用