知其所以然地學習(轉載)
知其所以然地學習(以演算法學習為例)
(轉載自劉未鵬的部落格C++的羅浮宮http://blog.csdn.net/pongba)
By 劉未鵬(pongba)
問題:目前幾乎所有的演算法書的講解方式都是歐幾里德式的、瀑布式的、自上而下的、每一個推導步驟都是精準制導直接面向目標的。由因到果,定義、引理、定理、證明一樣不少,井井有條一絲不亂毫無贅肉。而實際上,這完全把人類大腦創造發明的步驟給反過來了。看起來是陽關大道,實際上車馬不通。
而對讀者來說,這就等於直接告訴你答案&做法了,然後讓你去驗證這個答案&做法是可行&成立的。而關於答案&做法到底是怎麼來的,從問題到答案之間經歷了怎樣的思維過程。卻鮮有書能夠很好的闡釋。就我有限的閱(演算法)書經驗,除了波利亞的《怎樣解題》還算合格之外(也並非最理想),其它的(包括有名的《演算法導論》、《如何解題:現代啟發式方法》、《Algorithms
我們要的不是相對論,而是誕生相對論的那個大腦。我們要的不是金蛋,而是下金蛋的那隻雞。
Update(2008-7-24): 收到不少同學的批評,想來這個開頭對一些著作的語氣過重了,實際上,注意,我完全不否認這些著作的價值,我自己也在通過閱讀它們來學習演算法,並且有很多收穫。這篇文章更多的只是建議除了閱讀這些著作之外還需要做的功課。此外,對於這類知識講述(歐幾里德)方式的批判西方(尤其是在數學領域)早就有了,早在尤拉和龐加萊的時候,他們倆就極其強調思維的傳授,尤拉認為如果不能傳授思維,那數學教學是沒意義的。而龐加萊本人則更是對數學思維有極大的興趣和研究(我前陣子在討論組上還轉載了一篇龐加萊的著名演講,就是說這個的,參見這裡)。我只是在說目前的演算法書沒有做到思維講述的層面,因此建議閱讀這些書之餘應該尋找演算法的原始出處,應該尋根究底,多做一些功課,知道演算法到底是怎麼誕生的,並且我說明了為什麼應該知其所以然,有哪些好處(見下文),我還給了幾個例子譬如紅黑樹作者講紅黑樹的,g9
為什麼會這樣,其實是有原因的。
我們在思考一個問題的過程中有兩種思維形式:
* 聯想:這種思維某種程度上可以說是“混亂”的(雖然從一個更根本的層面上說是有規則的),所謂混亂是指很多時候並不確定聯想到的做法最終是否可行,這些聯想也許只是基於題目中的某個詞語、語法結構、問題的某個切片、一些零星區域性的資訊。這個過程是試探性的。最後也許有很大一部分被證明是不可行的。很多時候我們解決問題用的都是這種思維,簡言之就是首先列舉你關於這個問題能夠想到的所有你學過的知識,然後一一往上套看看能否解決手頭的問題。這種思維方式受限於人腦聯想能力本身的侷限性。我在《跟波利亞學解題》中就提到了幾個例子。聯想本身需要記憶提取的線索,所以受到記憶提取線索的制約,如果線索不足,那怎麼也聯想不起來。而提取線索的建立又取決於當初儲存記憶的時候的加工方法(《找尋逝去的自我》裡面有闡述),同時,面對一個問題,你能夠從中抽取出來的聯想線索又取決於你對問題的認識層度/抽象深度,表淺的線索很可能是無關的,導致無效的聯想&試錯(《Psychology of Problem Solving》裡面有闡述)。總之,聯想這個過程充滿了錯誤的可能。
* 演繹&歸納:演繹&歸納是另一種思維形式。它們遠比聯想有根據。其中演繹是嚴格的,必然的。歸納也是有一定根據的。在面對一個問題的時候,我們有意無意的對問題中的各個條件進行著演繹;譬如福爾摩斯著名的“狗叫”推理——狗+生人=>吠叫 & 昨晚狗沒有叫 => 那個人是熟人。就是一個典型的對問題的各個條件進行演繹的推理過程。還有就是通過對一些特殊形式的觀察來進行歸納,試圖總結問題中的規律。然而,不幸的是,面對複雜的問題,演繹&歸納也並不總是“直奔”問題的解決方案的。人的思維畢竟只能一下子看到有限的幾步邏輯結論,一條邏輯演繹路徑是否直奔答案,不走到最後往往是不知道的,只要答案還未出現,我們大腦中的邏輯演繹之樹的末端就始終隱藏在黑暗之中。而當最終答案出現了之後,我們會發現,這棵演繹之樹的很多分支實際上都並不通往答案。所以,雖然演繹&歸納是一種“必然”的推理,然而卻並不“必然”引向問題的結論,它也是試錯的,只不過比聯想要更為靠譜一些。
既然認識到,人類解決問題的兩大思維方式實際上都是有很大的試錯成分的(好聽一點叫“探索”),那麼就不難意識到,對一個問題的思考過程實際上是相當錯綜複雜的,而且充滿了無效分支——在思考的過程中我們也會不斷的對分支進行評估,做適當的剪枝——因此當我們找到問題的解之後,一來思維的漫長繁雜的過程已經在大腦裡面淡化得差不多了,只有那些引向最終結論的過程會被加“高亮”——我們在思考的過程中本就會不斷的拋棄無效的思路,只留下最有希望的思路。簡而言之就是最後證明沒用或者早先我們就不抱希望的一些想法就被從工作記憶中扔掉了。二來,思考過程是我們的空氣和水,而“魚是最後一個感覺到水的”,我們感覺不到思維法則本身的存在,我們只是不知不覺運用它。三來,由於我們的目標是問題的解,解才是我們為之興奮和狂喜的東西,而不是求解的過程,過程只是過程,目的才是目的。這就像一個尋寶者,在漫長曲折的尋寶曆程之後,在找到寶藏的時候,他會對寶藏感到狂喜(記得阿基米德的“找到了!”嗎?)而迫不及待地要展示出來,而漫長的思考本身卻成了註腳。我們是有目的的動物,目的達到了,其它的就相對不那麼重要了。最後,對於傳授知識的人,也許還有其四:感到介紹思維過程是不相干的,畢竟思維過程並不是演算法問題的解,演算法問題的解才是演算法問題的解。然而不幸的是,忽視到達解的那個過程實際上卻變成了捨本逐末。我們看到的是寥寥數行精妙絕倫的演算法,然後仰天長嘆自己想不出來啊想不出來。為什麼想不出來,因為你不知道那短短數行演算法背後經歷的事怎樣漫長的思考過程,如果問題求解是一部偵探小說,那麼演算法只是結局而已,而思考過程才是情節。
既然如此,也就難怪古往今來演算法牛人們演算法牛,但卻沒有幾個能真正在講述的時候還原自己的思維過程的(那個“ 漁”字),手把手的教學生走一遍推理的思路,就可以讓學生獲得思維過程的訓練。金出武雄在《像外行一樣思考,像專家一樣實踐》中說寫論文應該寫得像偵探小說一樣,我很贊同。歐幾里德式的介紹,除了提供枯燥的知識之外,並沒有提供幫助人獲得知識的東西——思維(關於對數學書籍的歐幾里德式寫法的批評其實也是由來已久了,並且有人呼籲了好幾種其它的教學方法)。從這方面,我們所尊敬的一些“聖經”級書籍在傳道授業上還不如偵探小說,前者是羅列一大堆知識,後者則是闡述獲得知識的過程——推理&聯想。
然而,我們都是人,人類該有的思維形式,我們難道不是都有嗎。既然如此,思維本身又有什麼需要一遍遍教的呢?
並非如此。
講述思維過程而非結果有幾個極其重要的價值:
* 內隱化:思維法則其實也是知識(只不過它是元知識——是幫助我們獲得新知識的知識);是內隱的記憶。我們在思考的過程中覺察不到思維法則的作用,它們卻在幕後實實在在的左右著我們的思維軌跡。要將思維方法內隱化,需要不斷練習,就像需要不斷練習才能無意識狀態下就能騎自行車一樣。
* 跨情境運用:思維法則也是知識記憶,是問題解決策略。既然是記憶,就受到提取線索的制約,這就是為什麼當波利亞告訴你要“注意未知數”之後你還是不能真正在所有需要你“注意未知數”的地方都能提醒自己“注意未知數”。很多時候未知數是很隱蔽的,未知數並不會總是頭頂一個大帽子上面寫著“我是未知數”。所以很多時候缺乏對這個策略的“提醒”線索,這也是為什麼你學會了在解決數學問題的時候“注意未知數”卻不一定能在解決現實生活中的問題中時刻都能“注意你的未知數”(《你的燈亮著嗎?》整本書的價值便在於此),因為解數學題和解決生活中問題的場景不一樣,不同的環境線索,在你大腦中激發的記憶也不一樣。就連問題求解中,不同的問題之間的細小差別也可能導致思維軌跡很大的不同,有時你的注意力會被一個無關線索激發的聯想吸引開去,忘記如“注意你的未知數”這樣的重要法則。而一本從思維角度來講問題求解的書則可以一遍遍將你置於不同的問題場景下然後在該提醒你的時候提醒你,讓你醒悟到“哦,原來這個時候也應該想到這個啊。”,做多了這樣的思維演習你就會逐漸從中領悟到某種共性,並將一些思維習慣得到強化,於是終於能夠在需要運用某策略的時候能適時的想起來了。
* 對問題解的更多記憶提取線索:我們平時學習演算法時幾乎僅止於“理解”,別人把一個方案放在你面前,你去驗證一下,心說“哦,不錯,這個的確可以工作”。然後就沒了。稍微簡單一點的演算法還好,複雜一點的對於記憶的負擔是很大的,這就是為什麼有時候我們看到一個絕妙的解法,這個解法看上去不知道從哪裡來的,但經過我們的理解,卻發現是對的,我們感嘆,真巧妙,結果一些天之後,別人問起這個問題,我們說:“唉,那是個多麼巧妙的演算法啊,但是我只記得它巧妙,卻不記得它到底是怎樣的了。” 為什麼?因為在不知其所以然的情況下,演算法只是一堆離散的機械步驟,缺少背後的思想的支撐,這些步驟之間就沒有一個本質層面上的關聯(先知亞里士多德早就指出:學習即聯接)。所以就跟背歷史書也沒多大區別。然而,知道了演算法是怎樣一步步被推匯出來的,我們就一下擁有了大量的記憶提取線索:對演算法發現過程中的任何一個關鍵步驟(尤其是本質)的回憶都可能使我們能夠自己動手推匯出剩餘的內容。譬如你知道堆(heap)是怎樣由樸素的決策樹演化而來的,它又是為了解決什麼問題的,你即便忘記了具體的細節,也可以自己推匯出來。譬如你知道KMP演算法的本質在於消除回溯,至於如何消除回溯卻並不是那麼難以推導的,所以即便忘了也可以藉助於大腦的邏輯演繹能力再現出來。譬如你知道Tarjan演算法其實只是從後序遍歷經過兩個優化調整而來的(其中並査集的使用其實只是優化手段——為了能夠迅速判斷祖先節點是誰——而非演算法本質 ——當然,演算法設計的主要任務本來就是通過問題條件中蘊含的知識來“消除冗餘計算”和“避免不必要計算”,所以你也可以說並査集的使用是關乎本質的,只不過,知道了為什麼需要引入並査集,就會強烈地感覺到一切是順理成章的了),那這個出了名的繞人的演算法也就不那麼難以理解和記憶了。譬如你知道排序的本質,就能夠對什麼是最優排序,為什麼它是最優排序有深刻的認識。四兩撥千斤。
* 包含了多得多的知識:記一個演算法,就只有一個演算法。一個蘿蔔一個坑。就好比背99乘法表只能解決乘法問題一樣。而記背後的思想,卻有助於解決一類問題。思想所處的抽象層面往往比到處都是實現細節的演算法本身要低,越是低的抽象層次,越是本質,涵蓋範圍越是廣泛。數學的發展本身就體現了這個過程,抽象代數就是非常好的例子。演算法誕生過程中的思路往往包含了比實際演算法更本質得多的知識,實際演算法乃至演算法的某個特定語言的實現包含了太多表面的不相干知識,它們會阻礙對本質的理解。
* 重在分析推理,而不是聯想:學了一大通演算法和資料結構之後的一個副作用就是,看到一個問題之後,腦袋裡立即不管三七二十一冒出一堆可能相干的資料結構和演算法來。聯想是強大的思維捷徑,在任何時候都會搶佔大腦的工作記憶,由不得你控制——比如我問你“如何尋找區間的最大值”,首先進入你的意識的肯定就是學過的那個演算法,甚至演算法的實現細節都一一跳了出來,也許最先跳出來的還是演算法實現中某個最容易弄錯的邊界細節,或是某個比較tricky的實現技巧!然而這些其實根本不反映一個演算法的本質,結果想來想去總是停留在問題的表層。而另一方面,重在思維的傳授則可以讓人養成從問題本質入手,逐步分析推理的習慣,而不是直接生搬硬套。當然,完全不可否認,聯想本身也是極其重要的思維方法,甚至可以說是人類思維最重要的特徵。很多時候我們並不知道問題的本質是什麼,就需要靠聯想、類比來領路探索。只不過,養成優先從問題的本質入手進行考察的好習慣絕對是有更大的好處的。
那到底什麼樣的才算是授人以漁的呢?波利亞的《如何解題》絕對算是一本,他的《數學的發現》也值得一看。具體到演算法書,那就不是光看text book就足夠的了,為了深入理解一個演算法的來龍去脈前因後果,從一個演算法中領悟儘量深刻的東西,則需要做到三件事情:
* 尋找該演算法的原始出處:TAOCP作為一個資料庫是絕對優秀的,基礎的演算法只要你能想到的,幾乎都可以在上面找到原始出處。查到原始出處之後(譬如一篇paper),就可以去網上搜來看了。因為最初的作者往往對一個方案的誕生過程最為了解。比如經典資料結構中的紅黑樹是出了名的令人費解的結構之一,但它的作者Sedgewick一張PPT,給你講得通通透透,比演算法導論上的講法強上數倍。
* 原始的出處其實也未必就都推心置腹地和你講得那麼到位:前面說過,演算法設計出來了之後人們幾乎是不會去回顧整個的思維過程細節的,只把直指目標的那些東西寫出來。結果就又是一篇歐幾里德式的文章了。於是你就迷失在一大堆 “定義”、“引理”、“定理”之中了。這種文章看上去整個寫得井井有條,其實是把發明的過程整個給顛倒過來了,我一直就想,如果作者們能夠將整個的思路過程寫出來,哪怕文字多上十倍,我也絕對會比看那一堆定義定理要容易理解得多。話說回來,怎麼辦?可以再去網上找找,牛人講得未必比經典教材上的差。那倘若實在找不出好的介紹呢,就只能自己揣摩了。揣摩的重要性,是怎麼說都不為過的。揣摩的一些指導性的問題有:為什麼要這樣(為什麼這是好的)?為什麼不是那樣(有其它做法嗎?有更好的做法嗎?)?這樣做是最好的嗎?(為什麼?能證明嗎?)這個做法跟其它的什麼做法有本質聯絡嗎?這個跟這個的區別是什麼?問題的本質是什麼?這個做法的本質又是什麼?到底本質上是什麼東西導致了這個做法如此..?與這個問題類似的還有其它問題嗎?(同樣或類似的做法也適用嗎?)等等。
* 不僅學習別人的思路,整理自己的思路也是極其重要的:詳見《跟波利亞學解題》的“4. 一個好習慣”和“7. 總結的意義”。