想不開的老爸:到底是誰在念小學?
兒子小四,老師出了一個數學作業:
一個4 x 4的方格,要把1到16的數字都填上去,已經知道4個角落的數字如上圖,分別為1,12,16,10。1到16的數字都必須填上去,不能少也不能多。而且填好的數字必須滿足各行相加後等於其行的加總值,還有各列相加後等於其列的加總值。可以看得出來,這題目的用意是要讓孩子們在遊戲之中,可以多多練習加法運算。老師用心良苦啊!
可那不爭氣的孩子,算了一個晚上都算不出來,來找老爸求救。
可憐,好不容易才脫離可怕小學生涯的老爸,只好硬著頭皮來算算看。
其實這題目不難,就只是將1到16的排列(permutation)都列示出來,每一種排列的可能都如圖般的排好後,再去計算是否符合各行列加總後是否等於各自行列加總值的條件,若符合即為答案,如此即可。
不過,聰明的我們都想找捷徑,例如我們看到第一列,就會想:"已經知道2個數字了,而且該列加總值為31, 所以第一列剩下的2個數字加總必為31-12-1=18"。同樣的,第4列剩下的2個數字加總必為49-16-10=23。第一行剩下的2個數字加總必為43-1-16=26。第四行剩下的2個數字加總必為33-12-10=11。然後就很高興自己找到一個捷徑,第四行。因為其剩下的2個數字加總應為11。而且1和10已經用掉了,所以只剩下以下4種組合(8種排列):
2 9
3 8
4 7
5 6
只要用這種方式,把第一行和第一列及第四列的可能組合也都列示出來,就可以大大簡化需要嘗試的次數。可是人類還是記憶容量太小的動物,要這麼多種可能的記錄和檢查,實在很容易出錯的。
學過程式的老爸實在懶得算,想想還是交給電腦算比較輕鬆。
不過太久沒寫程式了,都忘得差不多了。就找個不太需要安裝的python直譯器來寫就好了吧!那麼第一步是先產生所有的排列吧,google一下python產生排列的寫法,很好,網路上都有:
https://stackoverflow.com/questions/104420/how-to-generate-all-permutations-of-a-list
看到了,其實用python寫不難,人家程式庫都已經放在那等你拿去用,如上圖試了一下產生1到3的排列,果然很容易就做出來了。好,那試一下產生1到16的所有排列了。結果,記憶體不夠!哇哇!電腦都卡住了,少少的這幾行簡直就變成電腦炸彈,可以輕易的癱瘓任何一台電腦。那是需要多大的記憶體,才會這樣呢?
先算一下,1到16的排列的總數為16! =16 * 15 * 14 * 13 ... * 2 * 1
按一下計算機,16! =20,922,789,888,000
天啊!這是天文數字吧!20T,20萬億種可能。難怪記憶體不夠用,我就算再有錢,也不可能買到20TB的記憶體,就只為了解這個小小的數學習題。所以這個方案要修正一下,首先不能把全部的排列都存放在記憶體,這樣記憶空間會爆掉;接著,體認到排列數量龐大,所以要使用效率最高的工具,那就是古老的程式語言:C。
所以找一下怎麼用C來求排列:
https://www.geeksforgeeks.org/write-a-c-program-to-print-all-permutations-of-a-given-string/
太好了!很容易的樣子,這個範例會列示ABC三個英文字母的所有排列,就是有3!=6種排列。而且列印排列的程式行,就在if (l == r) printf("%s\n", a);這一行。
這樣,我只要在這一行檢查所有行列加總條件是否符合,若符合就找到答案。這樣做,不需要存儲所有排列,所以幾乎用不到什麼記憶體,只需要計算排列的算力而已。試著讓這支程式跑1到16的排列,並觀察其記憶體使用情況,果然幾乎沒用到記憶體。但讓我驚訝的是,程式跑了一個晚上,還沒跑完!
想想也是,就算電腦跑得再快,20萬億種可能都要一一比對,也是要跑很久的。所以要來做一下效能最佳化的工作了。想一下,其實已經有4個數字已知了,所以只要解12!=479,001,600種可能。哇哇!這樣馬上由20萬億變少成4億種可能。試了一下,果然程式可以在40秒左右就跑完了。
我把程式放在GitHub:
https://github.com/poushen/math_game1
首先看第一個版本:test3.c
程式裡都有寫註解了,如果題目有修改,就去改相應的地方就可以用在不同的題目。
比較要說明的是找到的C範例程式是使用A,B,C這樣的英文字母來玩排列,但我們需要的是1到16的數字來排序。其實也不用改什麼,因為電腦裡面英文字母其實本來就是數字編碼的,可以查一下ASCII這個編碼方法就知道。但是因為A的編碼為65, B的編碼為66, C的編碼為67,依此類推。而且C裡面有型別檢查,所以google一下,如何把字串轉成byte array, 就會看到test3.c裡面的string2ByteArray()這個function, 我將之稍加修改(- 64), 這樣就符合我們的需要了。
另外,題目本身是2維空間,但為了簡單起見,用找到的範例程式來排列,只用到1維空間的陣列。所以,在我們的腦袋裡,必須將題目的2維空間對應到實際上電腦裡的1維空間。如下圖般的,題目的16個格子,在電腦裡被編排了索引數字:這樣一來,我們只要告訴電腦:"請檢查A[0]+A[1]+A[2]+A[3]是否等於31",這樣就可以檢查第一列的列加總條件是否符合。同樣的方法,可以檢查第二列,第三列,第四列,只要改變索引數碼即可。對了,那行加總條件的檢查也是一樣的。"A[0]+A[4]+A[8]+A[12]是否等於43",這樣就完成第一行的檢查。其他幾行,就自己類推一下。編譯方法,執行方法,和執行結果如上兩圖,可以看到整個執行時間已經大大減少至10.18秒。而且答案其實在5秒左右就跑出來了。多工的用法在這裡其實很適用,因為排列是不會重複的,第一個格子的可能性有12種,正好用來分給12個小朋友(CPU核心)去賞試!
留言