1. 程式人生 > >卡特蘭數演算法題整理

卡特蘭數演算法題整理

最近我做了很多卡特蘭數相關的演算法題,閱讀了一些網路上的文章(本文最後有我找到的網文連結)。有的文章過於冗長,太糾結於公式的推導;有的文章又太簡單,只是羅列了相關的題目卻並無解題分析;而且這些文章或多或少都有一些錯誤。所以我寫了本文,目的是簡明的分析卡特蘭數題目的思路和解法,但又儘量不使本文變成沉悶的數學公式推導文章。

1. 卡特蘭數

卡特蘭數的具體定義,可以參考wiki https://zh.wikipedia.org/zh-hans/卡塔蘭數。本文只強調它的一個最重要的遞推公式。
假設C(n)為自然數n對應的卡特蘭數。若C(0)=1,C(1)=1,則有遞推式:
C(n)= C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)C(0) (n>=2)


例如:C(3) = C(0)*C(2) + C(1)*C(1) + C(2)*C(0) = 5
這個遞推式非常重要,下文的每一題都幾乎應用了這個公式。而且也正是這個遞推式讓我們可以用動態規劃或者卡特蘭數的通項公式求解卡特蘭數問題。

2. 演算法題彙總

1)假設一個二叉樹有n個節點,求所有異構二叉樹數量。
這道題來自 LeetCode 第96題 Unique Binary Search Trees。https://blog.csdn.net/tassardge/article/details/82916316 

選取任意一個結點為根,就把二叉樹切成左右兩個子樹,以這個結點為根的可行二叉樹數量就是左右子樹可行二叉樹數量的乘積。遍歷所有的節點並把所有可行結果累加,就得到了總數。寫成表示式如下:

C(n)= C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)C(0) (n>=2)

很明顯這正是卡特蘭數的遞推式。動態規劃的解如下:

class Solution {
public:
    int numTrees(int n) {
        if (n == 0)
            return 0;

        vector<int> res;
        res.push_back(1);
        res.push_back(1);

        for (int i = 1; i<n; ++i) {
            res.push_back(0);
            for (int j = 0; j<=i; ++j)
                res[i+1] += res[i-j]*res[j];
        }

        return res[n];
    }
};

以下各題的解法都與本題相同,所以下文不再列舉程式碼。

2)n對左右括號組成一個字串。求所有滿足以下條件的字串數:要求遇到左括號則入棧,遇到右括號則出棧;處理完後,棧為空。
我們把n對左右括號看作有2n+1個節點的二叉樹。並且使所有節點的左子節點對應左括號,右子節點對應右括號。這樣,除了根節點外的所有節點都與左或者右括號對應。對所有的異構二叉樹做遍歷就得到了所有滿足條件的字串。所以,這道題的本質是求異構二叉樹的數量。

例如,假設我們求3對左右括號有多少滿足條件的字串組合,則可求7個節點的異構二叉樹,並作遍歷即可。如下圖。

3)2n個人排隊買票,其中n個人持50元,n個人持100元。每張票50元,且一人只買一張票。初始時售票處沒有零錢找零。請問這2n個人一共有多少種排隊順序,不至於使售票處找不開錢?
如果把持50元的人看作左括號,持100元的人看作右括號,那麼這道題其實就是問題2)的變體。所以,這道題的本質是求異構二叉樹的數量。

4)n個數連乘,求所有可能的乘法順序。
我們把二叉樹的每個葉節點看作是乘數,那麼遍歷這顆二叉樹就得到了一種可能的乘法順序。所有可能的乘法順序就是求有2n+1個節點的異構二叉樹的總數。所以,這道題的本質是求異構二叉樹的數量。

例如,我們有4個數a b c d連乘,那麼轉換成二叉樹就是:

5)對於一個n*n的正方形網格,每次我們能向右或者向上移動一格,那麼從左下角到右上角的所有在副對角線右下方的路徑總數為多少。引用Wikipedia上的圖片:

如果向右走看作左括號,向上走看作右括號,那麼這道題其實就是問題2)的變體。所以,這道題的本質是求異構二叉樹的數量。

6)凸n+2邊形進行三角形分割數,要求只連線頂點對形成n個三角形。問有多少種不同的分法。
設邊數為n+2,選定任意一條邊作為第一個三角形的邊,再為它選擇一個頂點k,1<=k<=n。n邊型被分為了一個k邊形、一個三角形和一個n-k邊形。此時的劃分數為C(k)*C(n-k)。所以對所有的頂點遍歷後,有卡特蘭遞推式:
C(n)= C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)C(0) (n>=2)

7)求n個數的入棧出棧的排列總數。例如1,2,3的入棧出棧排序有123,132,213,231和321五種。
假設有2n+1個節點的二叉樹的每個非葉節點代表一個數字,那麼對所有異構二叉樹做遍歷,就得到了結果。所以,這道題仍是異構二叉樹問題。

8)求有2n個數字的集合{1,2,...,2n}的不交叉劃分的數目。
這裡解釋一下不交叉劃分。假設將集合{a,b,c,d}劃分為兩個子集{a,b}和{c,d},組成了兩個區間[a,b]和[c,d]。如果這兩個區間不重合,那麼以下四種情況是不交叉的:a<c<d<b,a<b<c<d,c<a<b<d與c<d<a<b,就是說兩個區間可以包含或者相離,那麼此時我們稱集合{a,b}和{c,d}是不交叉的。
對於集合{1,2,...,2n},將其元素兩兩分為一子集,共n個;若對於一個劃分,任意兩個子集都是不交叉的,那麼我們稱此時的這個劃分為一個不交叉劃分。如果此時我們將每個子集中較小的數用左括號代替,較大的用右括號代替,那麼帶入原來的1至2n的序列中就變成了合法括號問題,就是問題2)。

9)求n層的階梯切割為n個矩形的切法數。如下圖所示(圖片來自wiki):

我們先繪製如下的一張圖片,即n為5的時候的階梯:

我們注意到每個切割出來的矩形都必需包括一塊標示為*的小正方形,那麼我們此時列舉每個*與#標示的兩角作為矩形,剩下的兩個小階梯就是我們的兩個更小的子問題了,於是我們的C(5) = C(0)*(4) + C(1)*C(3) + C(2)*C(2) + C(1)*C(3) + C(0)*C(4)

10)向一個兩行n列的方陣中填入數字1到2n,求使每個方格內的數值都比其右邊和下邊的方格內的數值小的排列數。
該題的另一種問法:12個高矮不同的人,排成兩排,每排必須是從矮到高排列,而且第二排比對應的第一排的人高,問排列方式有多少種。

假設我們構造一個有2n+1個節點的二叉查詢樹(BST),除了根節點以外的所有節點都填上從1到2n的數字。後序遍歷該二叉樹便得到了一種排列方法。所以,這道題的本質仍然是求異構二叉樹。

3. 總結:

1)以上所有題目實際上都是先得到卡特蘭數的遞推公式,然後求解。
2)以上大部分題目很多都是二叉樹的變體,可見二叉樹的基礎有多重要。
3)以下是兩篇本文主要引用的文章的連結: