相信大部分同窗曾經都學習過快速排序、Huffman、KMP、Dijkstra等經典算法,初次學習時咱們驚歎於算法的巧妙,同時被設計者的智慧所折服。因而,咱們仔細研讀算法的每一步,甚至去證實算法的正確性,或者是去嘗試優雅地實現這些算法。總之,咱們會花費很大的時間精力去理解這些智慧的結晶。python
然而,如今對於這些經典的算法你仍然瞭然於胸嗎?就算如今你仍然記得這些算法的步驟,你敢確保一年後、十年後本身不會忘記?我想沒有多少人敢保證吧。算法
咱們固然但願本身掌握一個算法後,就永遠不會忘記,最好還能觸類旁通,利用算法中的思想去解決新的問題。然而,現實與美好的願景每每是背道而馳,不要說觸類旁通,咱們甚至常常忘記那些算法自己。數據結構
背算法與設計算法app
爲何會這樣?簡單來講,由於咱們歷來就沒有真正掌握過這些算法,咱們只不過是在背誦別人發明的算法,就像咱們背誦歷史書上的那些歷史事件同樣,時間久了天然會慢慢遺忘。學習
咱們接觸到某個算法時,看到的只是對算法過程的講解,對其正確性的證實,或者對其效率的分析(想一想大名鼎鼎的《算法導論》,《算法》是如何講解某一算法的),咱們不會看到那些牛人是如何「靈機一動」設計出了這驚天地泣鬼神的算法。也就是說咱們只是知其然,並無知其因此然。當咱們不知道一個算法的前因後果,不知道設計它經歷的那些思惟歷程時,就很容易忘記它的具體內容。相反,那些牛人就不會忘記本身設計的算法。測試
因此,當看到別人牛逼的閃閃發光的算法後,咱們必定要探尋算法背後那「曲徑通幽」的思惟之路。只有經歷了思惟之路的磨難,才配得上永遠佔有一個算法,並有可能觸類旁通,或者是設計一個巧妙算法。劉未鵬在知其因此然(三):爲何算法這麼難?中探索了Huffman編碼的思惟歷程,值得一看。順便說一下,探索算法背後的思惟歷程不是件容易的事,要知道就是霍夫曼本人也是花了一個學期纔想出它的編碼算法。編碼
下面咱們以LeetCode上一個好問題,來探索這個問題的算法背後的思惟之路。關於什麼是好問題,劉未鵬在跟波利亞學解題上有一個不錯的觀點:好問題即測試一我的思惟的習慣的題目,一般考察你的聯想能力、類比能力、抽象能力、演繹能力、概括能力、觀察能力、發散能力等。設計
一個好問題3d
LeetCode 84題:Largest Rectangle in Histogram,給定一個直方圖(下圖a),求直方圖中可以組成的全部矩形中,面積最大爲多少。對於圖a來講,咱們很容易看出來面積最大的矩形爲高度爲5和6的直方圖組成的矩形(圖b隱形部分),其面積爲5 * 2 = 10。排序
題目描述
其實這個題稍微加以變化,就是另外一個至關有趣的問題:Maximal Rectangle.
這道題目一個顯而易見的解決方法就是暴力搜索:找出全部可能的矩形,而後求出面積最大的那個。要找出全部可能的矩形,只須要從左到右掃描每一個立柱,而後以這個立柱爲矩形的左邊界(假設爲第i個),再向右掃面,分別以(i+1, i+2, n)爲右邊界肯定矩形的形狀。
這符合咱們本能的思考過程:要找出最大的一個,就先列出全部的可能,比較大小後求出最大的那個。然而不幸的是,本能的思考過程一般是簡單粗暴而又低效的,就這個題目來講,時間複雜度爲N^2 。那麼有沒有一種更加高效的解決辦法呢?
一個好算法
我第一次面對這個題時,並無想出一個漂亮的解決方案。由於從給定的條件來看,彷佛找不到一個約束條件使得知足這個條件的矩形面積最大,也就是說沒法縮減問題的規模,所以必須找出全部可能的矩形,這樣的話效率確定是N^2 。
然而去Google了一下,當即發現了一個時間複雜度O(n)的算法,當時就被這神奇的解法所震撼到。它的代碼十分簡單,簡單到一開始我根本就看不懂,不明白爲何這樣子求出的就是最大的矩形。網上好多所謂的解題報告裏面只是人云亦云地給出了算法的步驟,沒有算法正確性的證實,更沒有咱們最想要的關於解題思路。
我也先給出算法步驟和代碼,看看你是否是一樣一頭霧水。在程序中維護一個棧,棧中元素爲直方圖中bar的下標,而後從頭開始掃描每一個bar:
若是當前bar的高度大於棧頂bar的高度,則將當前bar的下標入棧;
不然執行出棧操做,記錄彈出下標對應的bar的高度,並計算出一個面積,而後用這個面積更新最大面積。
代碼也是至關簡潔,python源碼以下:
deflargestRectangleArea(self,height):
height.append(0)
size= len(height)
no_decrease_stack= [0]
max_size= height[0]
i= 0
whilei< size:
cur_num= height[i]
if(notno_decrease_stack or
cur_num> height[no_decrease_stack[-1]]):
no_decrease_stack.append(i)
i+= 1
else:
index= no_decrease_stack.pop()
ifno_decrease_stack:
width= i- no_decrease_stack[-1]- 1
else:
width= i
max_size= max(max_size,width* height[index])
returnmax_size
高效而難以理解,這就是那些神奇算法的共性。
一個思惟歷程
那麼這個算法真的就是我等凡夫俗子不能想出來的?難道咱們只能仰望高山,恨本身智商不高?我還真不服氣呢,因而又靜下心去思考這個問題。
此次咱們不從已知條件推結果,而直接從結論入手,就是說假設如今已經找到了面積最大的那個矩形。接着咱們來分析該矩形有什麼特徵,而後能夠用下面兩種方法之一來縮減問題的規模(由於這兩種方法都不用找出全部的矩形一一比較)。
找出知足這些特徵的矩形,面積最大的矩形確定是其中之一;
排除那些不知足這些特徵的矩形,面積最大的矩形在剩下的那些矩形裏面。
爲了使考慮狀況儘量全面,畫了許多直方圖,防止使用原題目圖片可能存在的一些特定假設,其中一個直方圖以下圖:
題目狀況分析
經過不斷地對多個直方圖的觀察,發現面積最大的那個矩形好像都包含至少一個完整的bar,那麼這條規律適用於全部的直方圖嗎?咱們用反證法來證實,假設某個最大矩形中每一個豎直塊都是所在的bar的一小段,那麼這個矩形高度增長1後仍然是一個合法的矩形,但新的矩形面積更大,與假設矛盾,因此面積最大的矩形必須至少有一個豎直塊是整個bar。
至此咱們找到了面積最大矩形的一個特性:各組成豎直塊中至少有一個是完整的Bar。有了這條特性,咱們再找面積最大的矩形時,就有了一個比較小的範圍。具體來講就是針對每一個bar,咱們找出包含這個bar的面積最大的矩形,而後只須要比較這N個矩形便可(N爲bar的個數)。
那麼問題又來了,如何找出「包含某個bar的面積最大的矩形呢」?對於上面的直方圖,包含下標爲4的bar的最大矩形以下圖橘黃色部分:
局部最大矩形
簡單觀察一下,就會發現要找到包含某個bar的最大矩形其實很簡答,只須要找到高度小於該bar的左、右邊界便可,上圖中分別是下標爲1的bar和下標爲10的bar。
至此問題已經變爲「對於給定的bar,如何肯定高度比它小的左、右邊界」。其實求左邊界和右邊界是一樣的求法,下面咱們考慮求每一個bar的左邊界。最直接的思路是對於每一個bar,掃面其前面全部的bar,找出最後一個高度小於它的bar,這樣的話時間複雜度明顯又是N^2 ,Holy Shit。
到這裏彷佛沒有路可走了,但若是咱們繼續絞盡腦汁地去想,可能(或許你對棧理解的很深刻,或許是你在一個相似的問題中用到了棧,固然你也可能想到動態規劃的思想,那也是可行的)會聯想到棧這一數據結構。用棧維護一個高度遞增的bar的集合,也就是說棧底到棧頂部對應的bar的高度愈來愈大。那麼對應一個剛讀入的bar,咱們只須要比較它的高度和棧頂對應bar的高度,若是當前bar比較高,則彈出棧頂元素繼續比較,直到棧頂bar比它低或者棧爲空。以後,將當前bar入棧,更新棧內的遞增序列。
咱們從左到右掃一遍獲得每一個bar對應的左邊界,而後從右到左掃一遍獲得bar的右邊界。兩次掃描過程當中,每一個bar都只有出棧、入棧操做,因此時間複雜度爲O(N)。經過這樣的預處理,便可以O(N)的時間複雜度獲得每一個bar的左右邊界。以後對於每一個bar求出包含它的最大面積,也便是由左右邊界和bar的高度圍起來的矩形的面積。再作N次比較,便可得出最終的結果。
這裏先預處理用兩個棧掃描兩次獲得左、右邊界,再計算面積,是按照推導過程一步一步來的。當咱們寫完程序後,再綜合看這個問題,可能會發現其實不必這樣分開來作,咱們能夠在掃描的同時,維護一個遞增的棧,同時在「合適的」時候計算面積,而後更新最大面積。具體實現方法就是前面給出的那個神奇的算法,不過如今看來一點也不神奇了,咱們已經探索到了它背後的思惟歷程。
固然,條條道路通羅馬,上面思惟過程只是其中一條通往解決方案的路徑,你可能以另外一種思惟過程找到了答案。不過,咱們上面的整個推導過程沒有涉及一些相似「神諭」的啓發,只是一些簡單的方法:好比從結論推導、反證法、概括總結、聯想(可能聯想到棧有點難)等,所以每一個人均可以學會,而且很容易被大腦記住。值得注意的是,咱們的整個思考過程並不簡簡單單地跟上面寫的那樣是線性的,它更多是樹形的,只是咱們剪去了那些後來證實行不通的枝。
解題的萬能思考法則?
人類在漫長的進化史中,解決了各類各樣的問題。例如
如何度過一條湍急的河流
如何保留火種
如何治癒天花
如何製造一個會飛的機器
…
同時也對本身的思惟方式進行總結和反思,笛卡爾曾經試圖將人類思惟的規則總結爲36條(最終完成了21條)。
那麼有沒有一個解題的萬能思考法則,按照這個法則去思考,最終能解決全部的問題或者是證實某個問題不可解?目前看來是沒有這樣的思考法則的,否則咱們就能夠製造出真正的會思考的機器了。
不過仍是有許多思惟方法值得咱們去學習強化,波利亞在《How To Solve It》上總結了這些方法,若是想培養良好的思惟習慣,那麼這本書是必不可少的。