靠山吃山,當然是藉助編程了。
我家的孩子上小學三年級,比較喜歡數學,課外在深圳上學而思的創新預備班。去年寒假開始我教他學習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億元的大買賣;咱們老鼠的兒子會打洞,程序員的兒子會寫代碼也不錯。再說了,以後他當個科學家了,也得會編程驗證自己的科學結果不是?