1. 程式人生 > >給定N個節點求組成二叉搜尋樹個數——從一道演算法題探討神奇的Catalan數

給定N個節點求組成二叉搜尋樹個數——從一道演算法題探討神奇的Catalan數

Catalan數,中文卡特蘭數又稱卡塔蘭數,是組合數學中一個常出現在各種計數問題中的數列。一旦入坑,你會發現這個數列相當有意思,能夠應用於很多看起來特別複雜的計算場景,當然,並能將之迎刃而解。

:卡塔蘭數是組合數學中一個常在各種計數問題中出現的數列。以比利時的數學家歐仁·查理·卡特蘭(1814–1894)命名。歷史上,清代數學家明安圖(1692年-1763年)在其《割圜密率捷法》最早用到“卡塔蘭數”,遠遠早於卡塔蘭。有中國學者建議將此數命名為“明安圖數”或“明安圖-卡塔蘭數”。
一般通項An=1n+1Cn2n=Cn2nCn12n

明安圖《割圜密率捷法》卷三 “卡塔蘭數”書影

我的“入坑”則歸功於幾天前在搜狐的實習生線上筆試上做到的一道題:key值分別為1,2,3,4,5,6的6個節點能夠組成多少中不同的二叉搜尋樹(BST)。試後,我在網上查到了很多對catalan數的討論,發現套用它的公式可以解決好多問題,甚至有不少問題都是網際網路筆試中老生常談的:

  1. n對括號 有多少種組合
  2. 矩陣鏈乘,依據乘法結合律,不改變其順序,只用括號表示成對的乘積,有幾種括號化的方案
  3. n個元素入棧 有多少種出棧順序
  4. 凸多邊形通過互不相交的對角線劃分,求劃分方案數
  5. 在圓上選擇2n個點,將這些點連線起來,使得所得到的n條線段不相交的方法數
  6. 2n邊的凸多邊形,連線對角線 可以分出三角形的個數
  7. n × n格點中不越過對角線的單調路徑的個數(上班路線選擇問題)
  8. 給定n個節點組成二叉搜尋樹個數(或組成的二叉樹形態數)
  9. 2n個高矮不同的人 站成兩排 保證後排對應的人比前排高 每排從左到 右越來越高 有多少種排列方式
  10. 《程式設計之美》4.3中的買票找零問題:2n個人排隊買票,其中n個人持50元,n個人持100元。每張票50元,且一人只買一張票。初始時售票處沒有零錢找零。請問這2n個人一共有多少種排隊順序,不至於使售票處找不開錢
  11. (騰訊筆試)在圖書館一共6個人在排隊,3個還《面試寶典》一書,3個在借《面試寶典》一書,圖書館此時沒有了面試寶典了,求他們排隊的總數?
  12. (阿里筆試)說16個人按順序去買燒餅,其中8個人每人身上只有一張5塊錢,另外8個人每人身上只有一張10塊錢。 燒餅5塊一個,開始時燒餅店老闆身上沒有錢。 16個顧客互相不通氣,每人只買一個。 問這16個人共有多少種排列方法能避免找不開錢的情況出現。

這類問題恐怕每道題單拿出來都是一道令人頭疼的演算法程式設計題,仔細觀察不難發現這些問題都是有一些共性的,比如都是求方案的個數,而且很多問題的應用場景都是一樣的,只不過在形式在做了變形,如:(1)和(2)是一類,(4)和(5)是一類,(9)(10)(11)(12)是一類。當然,在本質上,以上所有問題均能抽象為一種問題

:一種通解符合卡特蘭數列的問題。(實際上,根據求解思路,我把它歸納為兩類同構問題,這兩種思路都能推出卡特蘭數列,後面會討論到)

內容提要

本文將首先討論序列類場景的經典例題單調路徑問題及解法,並給出幾種常見的同構問題,然後以N節點二叉樹問題為切入點,介紹該類問題的一般解法及思考方式,最後給出卡特蘭數的一般性定義,總結卡特蘭數的數學思想。文末將附上相關參考文獻的連結

I.卡特蘭數性質

II.序列類場景:以N*N棋盤單調路徑問題為例

在比較了大部分常見的題型後,我發現很多例題所描述的問題都可以抽象為尋找符合若干條件的0-1序列的數量(具體有哪些條件後面我會講到),所以我將這一類問題歸納為“序列類”問題。前面提到我一共總結了兩大類,這並不意味這本類問題跟另一類是並列地位——實際上,本類問題只是對一類具體場景的概括(specific),而另一類是一般性解法(general)。
為什麼要單獨把它提出來講呢?因為這類問題的解法很巧妙,沒有構造遞推,直接得出通項公式,而且,也確實涵蓋了大部分筆試/程式設計題考點。很多人把這種解法稱為“折現法”(《程式設計之美》中貌似叫“反射法”)。

問題描述

求在N*N個格點中不越過對角線的單調路徑的個數,借用維基上的一張圖:
這裡寫圖片描述
左下角(0,0)點為起點,右上角(N,N)為終點。

分析

如果去掉“不能越過對角線”這個要求,我們能夠很容易的算出,單調路徑數為Cn2n,對於上圖情形即是C48。我們用X代表“向右走一格”,Y代表“向上一格”,則每條路徑可由字串String來表示,String滿足:

  1. String[i]=[X|Y]
  2. String.length=2n
  3. X與Y數量相等,均為n。
  4. 所有的字首字串(首項為String[0]的子串)皆滿足X的個數大於等於Y的個數

滿足(1)(2)(3)項的String的數量我們已經計算出為Cn2n個,現考慮計算該集合下不滿足(4)的情形的數量,然後減去該種情況,得到最終結果。

現從頭遍歷一個不滿足(4)的String,記為BadStr,當遍歷到第2m+1位上時有m+1個Y和m個X(容易證明一定存在這樣的情況),則後面剩下的部分中必有n-m個X和n-m-1個Y。
將第2m+2位及其以後的部分做以下變換:X變成Y、Y變成X,則該部分的X現在有n-m-1個,Y有n-m個,變換後字串記為cBadStr中共有n+1個Y和n-1個X的二進位制數。注意到,對於每個BadStr,均一一對應與一個這樣的cBadStr,因此NumOf(BadStrs) = NumOf(cBadStrs)=Cn12n
因此滿足(1)~(4)的String數量為Cn2nCn12n

這個結果就是傳說中的Catalan數

同構問題

出棧入棧問題

問題描述:對於一個無限大的棧,一共n個元素,請問有幾種合法的入棧出棧形式。

分析:令1表示進棧,0表示出棧,則可轉化為求一個2n位、含n個1、n個0的二進位制數,滿足從左往右掃描到任意一位時,經過的0數不多於1數。則結果我們可以用An表示

矩陣鏈乘(組括號)問題

問題描述:P=A1×A2×A3×……×An,依據乘法結合律,不改變其順序,只用括號表示成對的乘積,試問有幾種括號化的方案?

分析:將問題轉化一下,就是從左到右掃描,無論掃描到任何位置,左括號數一定要大於或者等於右括號數

買票找零(或借書)問題

問題描述:16個人按順序去買票,票價為50元,其中8個人每人身上只有一張50塊錢,另外8個人每人身上只有一張100塊錢。求要在售票員沒有初始金錢的情況下順利購票的排隊方案

分析:帶50塊錢的排前面的個數總是要大於帶100塊錢的人的個數,即C(16,8)-C(16,7)

照相排隊問題(阿里、騰訊筆試題)

問題描述:12個高矮不同的人,排成兩排,每排必須是從矮到高排列,而且第二排比對應的第一排的人高,問排列方式有多少種?在一個2*n的格子中填入1到2n這些數值使得每個格子內的數值都比其右邊和下邊的所有數值都小的情況數?

分析:這類問題稍微比以上的問題難理解點,它的Catalan數列“隱藏”的稍微深一些,需要我們做一些分析。
我們先把這12個人從低到高排列,維護一個12位的01序列,從左到右分別對應這12個由低到高排好序的人,用0表示對應的人在第一排,用1表示對應的人在第二排,其中0出現的個數和1出現的個數相等。

比如000000111111就對應著

第一排 0 1 2 3 4 5
第二排 6 7 8 9 10 11

010101010101就對應著

第一排 0 2 4 6 8 10
第二排 1 3 5 7 9 11

很容易證明,通過這種方式得到的兩排人,每排都是從矮到高排列的。那麼接下來的問題就是保證第二排比第一排對應的人高。
問題轉換為,這樣的滿足條件的01序列有多少個。
觀察每一個出現的1,在這個1前面,至少要有1個0,如果1前面還有1,那麼,1的個數一定應小於0,這樣就回到了上面的問題中,求01序列,滿足0的數量總是大於等於1。
擴充套件思考:如果問的不是排隊的可能方案數呢?如果讓你列印所有排隊方案呢?回溯法能夠很好的解決這類問題。
我在文末進行了更新,給出了程式碼求解方案。

III.一般性場景:由遞推公式求解卡特蘭數

該部分我會給出題目中的BST問題的解法(如果你是通過搜尋題目中的二叉樹關鍵字進來的,抱歉,現在才讓你看到你想看的,哈哈),然後試圖通過這類解法“感悟”卡特蘭數的基本數學思想,最後我會給出自己的理解,希望能給你也帶來啟發。

問題描述

求N個節點構成的不同構的二叉樹的個數/求N個大小不同的節點組成的二叉搜尋樹的不同形態數。
借用用wiki的圖:
這裡寫圖片描述

分析

  1. 先考慮只有一個節點的情形,設此時的形態由f(1)中,顯然,f(1)=1;
  2. 如果有兩個節點呢?現固定一個節點,那麼另個節點會有左右子樹兩種分佈情況,故有f(2)=f(1)+f(1);
  3. 再討論三個節點的情形,仍然固定一個節點,即根節點,然後此時還剩兩個節點,那麼左右子樹的總節點分佈情況為(2,0),(1,1)和(0,2),f(3)=f(2)*f(0)+f(1)*f(1)+f(0)*f(2)。f(0)表示什麼也沒有,不會增加額外的情況,故取值為1;
  4. 那麼對於n個節點呢?同樣,固定一個節點,那麼左右子樹的分佈情況為(n-1,0),(n-2,1),(n-3,2)……(1,n-2),(0,n-1),故f(n)=f(n-1)f(0)+f(n-2)f(1)+……+f(1)f(n-2)+f(0)f(n-1)

當得出這個公式的時候,相信大家以及明白接下來怎麼做了吧。交給程式去遞迴就好啦。這裡就不再多說。而這個公式也正是卡特蘭陣列的遞推公式。

總結

卡特蘭數的遞推公式是:
C(0)=1;C(1)=1;
C(n)=C(0)*C(n-1) + C(1)*C(n-2) + …… + C(n-1)C(0);
其實序列類場景也是可以用這種一般性方法求解的,大家有沒有發現,無論是上面的排隊問題,多邊形分割問題,還是路徑規劃問題,它們都有一個共同點,就是初始狀態一定是確定的,也就說,序列第一項是固定的,那麼剩下的n-1項就可以用分治的思路分割成兩個子序列去解決就可以了。抽象點概括下,就是對於問題A,規模為n,要解決這個問題,可以用分治的思想,首先固定其中某一個元素,將剩下的n-1個元素拆分成兩個小問題,這兩個小問題的規模分別是(0,n-1) (1,n-2) (2,n-3) … (n-1,0)。
卡特蘭數表現了一種符合乘法原理事件的本性,某種程度上,反映了我們思考問題的方式,故而能夠在許多場合得到應用。
卡特蘭數的遞推公式可以表示為:

An+1=ni=0aAiAni

卡特蘭數的通項是

An=1n+1Cn2n=Cn2nCn12n

至於如何由遞推公式,推出通項的,一種思路就是我在第二部分中講到的“折線法”,但是是通過構造具體問題推匯出來的,很巧妙,看完之後我也是回味了半天。但是如果拋開這些問題,直接給你個遞推公式,然後讓你求通項公式,怎麼下手去做呢?

有沒有純數學角度的推算方法呢?
折線法和遞推求法的本質聯絡在哪呢?

這裡留給讀者,同樣也是留給我的一個開發性問題。如果誰要比較好的證明思路,歡迎私信給我哈。
我的郵箱是:[email protected]

最後,給大家推薦一個奇妙的網站
The On-Line Encyclopedia of Integer Sequences,網址是oeis.org,對數列有興趣的同學歡迎戳進去感受數學之美,

更新2017-5-19:

今天在牛客上做到一個題,用到了卡特蘭數的知識。
那道題我會單獨寫一篇部落格,這裡把其中一個子問題抽象出來,給出程式碼方案:
給了長度為2n的順序數列,先將數列分成兩排,要求第一排的每一列小於等於對應的第二排的數字,每排順序排列,打印出所有排序方案。

這裡我用回溯方法解決,對所有方案進行深度優先遍歷,如果‘0的數量大於1’(參加上文排隊問題),則返回上層。
這裡我構造了兩個空數列:firstLine和secondLine,從頭到尾遍歷長度為2n的原陣列arr,每次都有兩個選擇方案:將arr[i]放入到firstLine或secondLine。怎麼放呢?沒關係,我們先進行遍歷,按順序來,如果放入後firstLine的長度大於等於secondLine,則進入下層迴圈。另外,為了保證回溯,一定要記得及時清理狀態,在每次迴圈後將兩個陣列的狀態返回為上一層的樣子。

程式碼如下:

function catalanSort(arr,firstLine,secondLine,i){
    var n=arr.length/2;
    if(firstLine.length==n) {
        console.log(firstLine);
    }
    else {
        for (var j = 0; j < 2; j++) {
            if (j == 0) {
                firstLine.push(arr[i]);
            } else {
                secondLine.push(arr[i]);
            }
            if (firstLine.length >= secondLine.length) {
                catalanSort(arr,firstLine,secondLine,i+1);
            }
            if(j==0){
                firstLine.pop();
            }else{
                secondLine.pop();
            }
        }
    }
}

呼叫方式就是:

catalanSort(arr,[],[],0)