?? youxi.doc
字號:
for i in seq:
product = int(i[:where]) * int(i[where:])
if product > maximum:
maximum, max_item = product, i
elif product == maximum:
max_item += ','+i
rev = list(i)
rev.reverse()
i = ''.join(rev)
product = int(i[:where]) * int(i[where:])
if product > maximum:
maximum, max_item = product, i
elif product == maximum:
max_item += ','+i
print "Maximum at", max_item, ",product", maximum
當然你保留了以前的函數 calc 而只是新加一個專門給 permute7.py 調用的 calc2 函數. 你試了一下速度, 成功了比 permute6.py 快了一丁點. 雖然只是這一點兒點兒, 你也覺得快活無比. 因為又一次, 你為一個大家都覺得好的方法做出了改良.
雖然你知道在這個階段如果你把 calc.py 整合到排列產生器中也許會得更好的提速效果, 但你覺得現在這樣已經可以很多人都認同你的能力了. 而且能把一個高效的排列產生器獨開來也許是聰明的做法, 因為將來你一定會再用它的. 你看了一下所有的程式, 從 permute1.py 到 permute7.py, 再做了一次速度的檢定. 反正是最后一次了, 干脆做個大的吧.
$ time python permute2.py 123456789 5
Got 362880 items.
Maximum at 875319642 ,product 843973902
real 0m46.478s
user 0m46.020s
sys 0m0.430s
$ time python permute3.py 123456789 5
Got 362880 items.
Maximum at 875319642 ,product 843973902
real 0m38.997s
user 0m38.600s
sys 0m0.400s
$ time python permute4.py 123456789 5
Got 362880 items.
Maximum at 875319642 ,product 843973902
real 0m33.845s
user 0m33.460s
sys 0m0.380s
$ time python permute5.py 123456789 5
Got 362880 items.
Maximum at 875319642 ,product 843973902
real 0m10.681s
user 0m10.530s
sys 0m0.150s
$ time python permute6.py 123456789 5
Got 362880 items.
Maximum at 875319642 ,product 843973902
real 0m8.279s
user 0m8.110s
sys 0m0.170s
$ time cpython permute7.py 123456789 5
Got 181440 items.
Maximum at 875319642 ,product 843973902
real 0m7.827s
user 0m7.650s
sys 0m0.180s
嗯, 很不錯. 最快的要比原先快上近七倍呢! 於是你打算明天一有空便把這個最好的公式貼到網上去, 讓更多人分享. 你很放心地去睡覺了.
但是不知為了什么, 你總覺得有些事煩擾著你, 還有什么不妥的地方呢? 你實在想不到了, 迷迷糊糊地抱著迷惑不安的心情入夢.
終於你忍不住爬了起床, 再一次回到電腦屏幕前. 你想到了一個致命的問題, 對於很大很大的排列, permute7.py 還是會嘗試一下子把所有的排列都做出來, 不用多久電腦資源就會被耗光的. 你也許要另想一個方法, 每次只做一個排列. 但你是否可以把所有的排列用 1, 2, 3, 4 的方法數出來呢?
你看著教科書上的那幅圖畫, 這樣的樹狀結構應該有辦法的, 你對自己說.
慢慢讀著書上的文字. 設想有 n 個數字, 先取第一個數字. 再取第二個數字, 第二個數可以放在第一個數的左或右面, 就是有 0, 1 兩個選擇. 再取第三個數, 放到前面選好的兩個數字中, 可以放在最左, 中間, 最右, 就是有 0, 1, 2 三個選擇. 嗯, 很自然嗎. 忽然你想到了二進位, 八進位那些數系轉換關系. "我可以設計這樣一個數, ...xyz, 其中個位數 z 是二進位的, 也就是放第二個數的兩個位置; 十位數 y 是三進位的, 化表放第三個數字的三個位子, 然后百位數是四進位, 千位數是五進位的, 依以類推." 沒錯, 這樣設計的話, 如果 0 表示放於最左面的話, 則 "2021" 這個數就代表了排列五個元素 (abcde), 取一個 a, 然后第二個 b 放在 a 的右面成 ab, 取 c 放到最右面成為 abc, 取 d 放到最左面成 dabc; 最后 e 放到中間去成為 daebc. 至於 "2021" 這個特別的設計的數可以用 ..+ x*4 + y*3 + z*2 這樣的計算來映對到自然數的數列上去.
沒錯了, 如求 4 個數的 4! = 24 個排列, 第 18 個排列可以這樣求得, 18 除 2, 余數是 0, 所以第二個數放在第一個數的左面; 然后商 9 再除 3, 余數 0, 所以第三個數於在頭兩個數的最左; 最后 3 除以 4, 余數是 3, 因此第四個數要放在前三個數的第 4 個空位, 也就是最右面. 利用這個方法, 你就不必先求出整個排列才能開始計算. 盡管這好像犧牲了時間, 但省下了大量的空間. 你完全可以算出 1000 個數的最大排列方法, 縱使那可能要用去幾個月的運算. 你更高興的是用這個方法, 你可以把整個計算拆開成為一個一個的部份: 譬如說求 10! = 3628800 個排列, 你大可以把 1 到 1000000 讓一部電腦做, 1000001 到 2000001 讓另一部來做 ... 大家的工作并不重覆, 這等於實現并行運算了! 啊哈! 妙極了!
忽然你靈光一閃, 對了, 這個不正是 permute4.py 的算法嗎! 你昨天還久思不得其解呢, 現在你已經完全明白了. 嗚, 雖然這么好的算法原來不是你原創的, 但是你仍然異常興奮. 因為你的思路竟和那些大牛們互相吻合. 你漸漸記起了當你還在玩 DOS 游戲機的年代, 曾有個古怪的人向你吹噓過某個電腦撲克游戲中用了一個威力很大的洗牌法, 多么的快而且公平, 不必怕黑客們用已知的隨機數表來出千. 現在你猜到了, 那個算法很可能就是眼下這一個了. 有 52 張牌, 如果要預先算出 52! 個排列才能洗牌可真要命呢.
你覺得舒服多了, 你整理了一下程式, 打算把結果告訴大家. 然而剛才的不安感又重新來襲. 你再看了一次最后也應該是最棒的程式, 心中安慰自己道: "千萬不要跌進低等程式員的的陷阱, 他們瘋也似的不斷追求, 郤從來不知道自己的目標, 也不知道什么才是好. 完美的設計不在于你無 法添加新的功能, 完美的設計是再也不能精簡現有的功能." 你覺得 permute7.py 已迫近了這一個 極限. 但你內心深處并沒有因此而舒坦開來, 一種懸空的感覺自足下升起. 也許是你太累了, 于是 者你決定閉目養神數分鐘再開始上網, 可惜你很快地迷迷糊糊地進入了夢境.
待續...
終篇:
你做了一個夢, 夢中你看到阿凡提騎著他那出名的毛驢來到你面前并向你提出挑戰: "除非你解答了我的難題,不然我的驢子會不停在你耳邊嘶叫令你無法睡好! 問題是: 把數字 56789 放到 [][][]*[][] 里得出最大的的乘積...." 你發出會心的微笑, 正想祭出你的 permute7.py 之時忽然想起阿凡提是不可能懂得電腦編程的! 你心中登時涼了一大截: 阿凡提的方法一定不必用電腦算出所有的排列方法, 并很快的得知答案的. 隨著一聲震天的驢嘶, 你驚醒了, 發現原來你伏在電腦桌上睡去了, 不小心壓著了鍵盤上的方向鍵而令你的電腦發出了痛苦的 BEEP 聲.
回想夢境, 你打算暫時離開電腦, 回到問題本身上來: 怎樣才能"看"出最大的乘積呢 ?
你拿出紙筆, 開始計算:
假設五個數為 [a][b][c]*[d][e], 展開的話成為
a * 100 * d * 10
+ a * 100 * e * 1
+ b * 10 * d * 10
+ b * 10 * e * 1
+ c * 1 * d * 10
+ c * 1 * e * 1
這個可以寫成一個矩陣:
d e
a 1000 100
b 100 10
c 10 1
你這樣想到: 在整個答案中, a 帶來的頁獻是一個百位數加上一個十位數, 而 d 的頁獻是一個百位數加十位數加個位數, 因此 d 要比 a 更重要. 要取得最大的積則一定要把 56789 中最大的 9 放在 d 的位置, 然后是 a, 如此類推.
為了方便計算,你干脆用對數來記數 100 = 10e2, 用 2 來代表好了, 因此:
d e
a 3 2
b 2 1
c 1 0
計算每一行或列的和, 把它稱為該數的基值, 我們得到
a = 5, b = 3, c = 1, d = 6, e = 3.
咦? b 和 e 的基值是一樣的, 怎么辦!
你思考著: "啊哈! 因為我們用了對數, 而 log(1) = 0 因此把 b 和 e 之間的微小分別給忽略了!" 好吧, 試把每個數都加大一個, 得到:
d e
a 4 3
b 3 2
c 2 1
這樣基數變成了: a = 7, b = 5, c = 3, d = 9, e = 6. 這些基數代表了該位置的重要性, 可以按大小分予不同的數字.
好, 按基數的大小來分配數字你得到了 865 * 97. 一對答案, 喲! 不一樣! 正確解是 875 * 96. 哪里不對了呢? 仔細分析下來, 你發現 b 和 e 互換了. 換的原因是這樣的:
b 的頁獻: b * d * 100 + b * e * 10 e 的頁獻: e * a * 100 + e * b * 10 + e * c
粗看的話 e 的頁獻要大一些, 但因為我們把 9 配給了 d 而 8 配給了 a, 因此最終的結果是 b 的實際頁獻要比 e 大. 由於 e 和 b 的基數只相差在 e * c 這個個位數乘積上, d 和 a 之間的分配結果把這個小的差異覆蓋掉了.
你考慮著: "要把這樣的覆蓋也算上去的話, 也許可以做一個二階基數. 如 b * d 的基數是 100, 但是由於 d 的基數為 9, 那 b 的二階基數可以算成 9, 代表和 b 相關的是一個較為重要的數; 同樣 e * a 的基數是也是 100 但由於 a 的基數只是 7, 因此 e 的二階基數只是 7 而已. 這樣就可以得出 b 要比 e 更重要了."
於是你有了一個想法: 先寫出相關矩陣, 然后計算每個數的基數和二階基數, 再進行排序, 當兩個基數很接近時就用二階基數來判別哪個較重要. 嗯, 你覺得自己聰明極了, 於是開始驗算, 但很快又發現其實 b 和 e 的二階基數原來也是一樣的!! 大家都是 15. 也許我們要用一個三階基數才能分辨他們.
你又想了一些新的二階基數的定義, 有些的確給出正確的答案, 但你漸漸覺得這一切并不很妥當. 因為就算能計出 56789, 但是在更多的排列, 如 7 位數甚至 9 位數的排列你怎樣保證答案也一定準確呢, 而兩個基數到底怎樣才算是比較接近呢? 仔細審視你的方法, 用對數來表示乃至直接相加來求所謂的基數統統都是即興的, 毫不嚴謹. 或者要真正解決他們必需要把每一種情況都算進來, 而那樣做的話必然要計算 n! 那么多次! 說到底還是要算排列的.
你有些失望, 但暗中覺得松了一口氣, 因為到底是 permute7.py 得到最后的勝利. 你伸了一下懶腰, 原來天都快亮了. 這時你感到有些餓, 便去拿了半個涼饅頭, 沖了一些可可. 你對著空空的螢光屏, 靜靜地坐了大概十分鐘, 然后答案進入你的腦海, 謎團被解開了.
你的方法是求出所有位置的"重要性"(用你的語言就是求基數), 然后依次把最大的數字分配給最重要的位置. 但是位置的重要性是和其他位置糾纏在一起的, 因此一次過算出所有位置的重要性必須考慮大量不同的組合排列, 并不實際.
但是, 我們其實可以每次只求第一個最大的基數的位置 (abc*de 的情況下最大的基數是 d), 這個最大的基數是沒有爭議的. 當求得這個位置時, 干脆把最大的數字放到該位子上, 然后再求一次基數, 找出接下來最大的位子, 這個位子也是無可爭議的. 如此一來, 原來求 5 個數字的全排列成就簡化為 5 次簡單的回圈. 一個求 Factorial(n) 的問題變成了 n 次循環!
啊哈!
你精神大好, 從另一個角度切入:
假如 5 個數字一樣, 11111, 最大的乘積只能是 111 * 11, 現在容許改大一個數, 改哪個會使結果最大 ?
211 * 11, 121 * 11, 112 * 11, 111 * 21, 111 * 12 ? 答案是 111 * 21, 也就是 d 的位置. 好, 把 d 替換成 9.
問題變成 5 個數字, 111 * 91, 改一個數(除了 9), 改哪一個 ?
211 * 91, 121 * 91, 112 * 91, 111 * 19 ? 答案是 211 * 91, 也就是 a 的位置. 好, 替換成 8.
依此類推, 答案正正是 875 * 96.
你重開電腦, 很快地把新方法輸入程式, 并改名為 wise.py.
1 def solve(seq,where):
2 n = len(seq)
3 seq.sort()
4 seq.reverse()
5 table = [ [] for i in range(n) ]
6 left, right = where, n - where
7 leftr = long('1'*left)
8 rightr = long('1'*right)
9 flag=[]
10 for item in [ int(x) for x in seq]:
11 for i in range(left):
12 table[left-i-1] = (leftr + 10**i) * rightr
13 for i in range(right):
14 table[right-i+where-1] = leftr * (rightr + 10**i)
15 for i in flag:
16 table[i] = 0
17 tablesorted = table[:]
18 tablesorted.sort()
19 maxindex = table.index(tablesorted[-1])
20 if maxindex >= where:
21 rightr = rightr + (item-1) * 10**(right-maxindex+where-1)
22 else:
23 leftr = leftr + (item-1) * 10**(left-maxindex-1)
24 flag.append(maxindex)
25 #print maxindex, leftr, rightr
26 return leftr, rightr
27
28 import sys
29 leftr, rightr = solve(list(sys.argv[1]),int(sys.argv[2]))
30 print "Maximum at", leftr,rightr, ',product', leftr*rightr
你驗算了一下結果, 完全正確! 這時你好奇地再次 time 了一下程式的速度
$time python permute7.py 123456789 5
Got 181440 items.
Maximum at 875319642 ,product 843973902
real 0m7.827s
user 0m7.650s
sys 0m0.180s
$ time python wise2.py 123456789 5
Maximum at 87531 9642 ,product 843973902
real 0m0.042s
user 0m0.010s
sys 0m0.030s
哇! 快了近兩百倍! 當然了. 如果算更多位的排列會快更多, 因為 wise.py 跳離了 n! 的限制.
你現在覺得舒服多了. 你真的解了這個問題. 你不再怕有人會寫出更快 10 倍的程式了. 你既有了"聰明"的答案 (軟解) 來對付阿凡提和他的驢子, 而在硬解方面, 你也自信有世界第一流的排列產生器. 你完全滿足了, 你不再感到疲累, 心中疑猶一掃而空. 這時你身體感到一陣震栗但心中卻喜樂無窮, 你第一次感受到了編程之道的洗禮. 并且, 你學會了所有程式大師都有的態度: 我沒法用中文來形容, 這種態度叫做 "to hack". 你知道只要你熟練并保持這種態度來面對生命中的難題, 你很快就可以滿師出山了.
你最后一次瀏覽了一下你的程式碼, 發現在 wise.py 中, 其實每一個循環完成后, 最重要的位置和最次要的位子都是不容爭議的, 因此大可放心地替換兩個數字而不是一個, 那程式可以再快一倍. 不過你覺得現在己經很夠了, 你頗有禪機地自言自語道: "我已經找到明月,再糾纏只下去只是妄執於指月的手而已." 你熟練地登出系統并關上電腦, 你知道這次你可以真正安心地睡一覺了.
哎喲! 天已亮了, 今天是禮拜一, 你要上班的. 喔! 又要被老板說上班一條蟲, 下班一條龍...... 慘.......
全篇完.
課后檢討:
一) 在上面的故事中,我們看到了解決編程問題的五個方法.
1. 把問題規范成一個普遍的形式,這樣更容易和別人交流以及找相關資料.
2. 自己嘗試找答案.
3. 在網上搜尋更好的答案.
4. 想一個方法來打敗這個更好的答案.
5. 翻查教科書或是文獻,從基本開始思考有沒有最好的解.這些書能被選成教本一定有它的原因.
6. 研究問題的特殊情況, 也許會有別出心裁的巧妙方法.
二) 故事中每個程式都只有二三十行大小,說明 Python 語言表達力強且語意很濃縮, 做為快速開發或是測算自己的想法都是非常好的.
三) Python 程式濃縮之余,它的語言也異常的清晰.回看上面的程式,你會發現它們全都不難明白.這說明 Python 程式更加容易維護.
四) 在故事中,我們有很大的篇幅是在討論方法而只有小部份是在描述 Python 的語言特性.這證明 Python 更適合用來教授編程的概念. 事實上, Python 的作者 Guido 和很多人都認為 Python 是電腦教育的首選語言. 教師可以讓學生靜靜地思考,學通運算的法則; 而不是上來就瘋狂地敲打鍵盤,并要記住一大堆電腦語言的古怪特徵.
五) 整個故事圍繞於算法的改進而較少碰到 Python 程式的優化問題. 也許在續集中(如果有的話), 我們要嘗試一下在固定的算法及盡量少改動程式碼的條件下, 提高 Python 程式的效率. 我暫時想到的方法包括:
1. 利用較新和較快的語法. 如 yield, generator.
2. 用 Python 的自帶優化選項以及內建模組.
3. 用第三方的擴展模組, 如 Numpy, Scipy.
4. 用編譯方式代替解釋, 如 freeze, py2exe.
5. 用 JIT 類的方法, 如 Psyco.
6. 用 Thread, 在多 CPU 的機器上平行運算.
7. 最后一樣要大改程式了, 用 C 來做擴展.
8. 更有 'to hack' 感覺的, 修改 Python 主干程式, 加入像 string.reverse() 這樣的輔助函數.
六) 文中所用的測試硬件:
CPU: Pentium III 866 RAM: 128 MB
文中所用的測試軟件:
Slackware Linux: 9.0 Linux Kernel: 2.4.2 GCC: 3.2.2 Python: 修改過的 2.1.3
七) 啃涼饅頭對腦筋有幫助.
八) 如果你能想到更好的方法, 歡迎聯絡本人: glace_at_chinesepython.org
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -