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