青少年編程如火如荼,有人說數學好編程就好,也有人說編程好數學也差不了。沒錯,兩者有緊密關聯,相輔相成,但也有一定的區別。下面是我和昍一起討論過的編程書上的幾個例子,都是從數學和編程兩個角度來思考,也算是一種別樣的嘗試。從中,可以瞥見編程思維和數學思維的差別。
例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<<i<<endl;
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<tride)
cout<< 「走路快」<<endl;
else if(twalk>tride)
cout<<」騎車快」<<endl;
else
cout<<」一樣快」<<endl;
編程思維就和我們大部分人考慮問題的方式差不多,直腸子,要什麼,就求什麼。當然,上面的程序之所以說是基本上是這樣,是因為還有點小問題。問題在於計算機本身的限制:受制於表示的位數限制,計算機不能精確地存儲一個高精度的小數,例如一個無限循環小數或無理數,那麼當兩個數非常接近時可能會出錯。
一個改進的做法是,儘量不做除法。我們可以把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<b)
cout<<」大洋商城」<<endl;
else
cout<<」百匯商廈」<<endl;
數學做法:簡單分析一下,買八送一最划算的就是買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 << 「雞=」<<i<<「,兔子=」<<35-i<<endl;
可以看到,編程做法更類似於我們數學中的驗算。本質上是用方程建模,用枚舉求解。
例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<<s;
數學做法:按數學思維,一開始就要把這個問題抽象化為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),後面就不介紹,學過等差和列項應該有所了解。
昍爸:曾獲初中和高中全國數學聯賽一等獎,江蘇賽區第一名,高考數學滿分,現為大學計算機專業教授,
公眾號:xuanbamath