逆波蘭式與卡塔蘭數

來源:互聯網
上載者:User

  讀了我的上篇文章“一個 Lua 的湊24程式”的讀者可能會產生這樣的疑問:

  1. 為什麼由 4 個數字和 3 個運算子組成的合法的逆波蘭式就只有那 5 種?你是怎麼窮舉的?
  2. 為什麼不寫程式自動求出所有的合法逆波蘭式,而非要手算?

  為了討論這兩個問題,我們先來看下面的問題:

 

問題1(出棧序列問題):數 1 ~ n 按順序入棧(棧是後進先出的),任何時刻你可以選擇讓下一個數入棧,或者讓當前棧頂元素出棧(若棧非空)。求所有可能的出棧序列的個數。

例:

若 n = 2,有兩種:

  1. 進進出出,得到出棧序列:2 1
  2. 進出進出,得到出棧序列:1 2

若 n = 3,有五種:(下面以 1 表示進棧,-1 表示出棧)

  1. 1 -1 1 -1 1 -1,得到:1 2 3
  2. 1 -1 1 1 -1 -1,得到:1 3 2
  3. 1 1 -1 -1 1 -1,得到:2 1 3
  4. 1 1 -1 1 -1 -1,得到:2 3 1
  5. 1 1 1 -1 -1 -1,得到:3 2 1

  我們注意到:n = 3 時的出棧序列有 5 種,正好跟 4 個數字和 3 個運算子構成的逆波蘭式個數相等!

 

  於是我們猜想:這兩個問題是等價的

 

  為了證實這個猜想,我們把出棧序列問題改寫成下面的形式:

問題2(1,-1排列問題):將 n 個 1 和 n 個 -1 排成序列 S2n,使得對任意的 k ∈[1, 2n],滿足,。求所有排列的個數。

  注意到在問題1中,若出棧入棧操作的排列順序不同,則最後得到的數字序列也一定不同,因此出棧序列數就等於操作的排列個數,於是問題1與問題2等價

 

  回顧逆波蘭式的計算過程:每當遇到一個數字,就入棧,相當於棧中元素個數 +1,而每遇到一個運算子就出棧兩個數字,運算後結果入棧,相當於棧中元素個數 -1。我們還要求中間任何一步時棧中元素個數≥1。

 

  如果把入棧操作(或 1)對應於逆波蘭式中的數字,出棧操作(或 -1)對應於逆波蘭式中的運算子,我們立即得到:

 

  n + 1 個數字和 n 個運算子組成的合法逆波蘭式的個數問題,與 n 個數的出棧序列問題等價。不同之處在於逆波蘭式問題中,棧裡始終有一個數字。

 

  再做進一步聯想,逆波蘭式相當於對錶達式樹做後序遍曆,於是我們不禁要問:逆波蘭式的個數問題與運算式樹狀架構,或者說二叉樹的個數問題是否等價?

問題3(二叉樹計數):求 n 個結點的二叉樹的個數。

 

例:當 n = 3 時,有 5 種:

  為了進一步討論這個問題,我們先來看一下如何求 n 個數的出棧序列的個數 Cn。

 

  為此我們將 1,-1 排列問題改寫成下面的 x ≥ y 格路問題。

 

問題4(x≥y格路問題)在 n * n 的方格中每次只能要麼向右走,要麼向上走。在 x ≥ y 的地區中,從 A 到 B 有多少種走法?

 

  將 1 與向右對應,-1 與向上對應,不難看出這兩個問題的等價性。

 

  下面就來求解這個問題。

 

  設走法數為 Cn,考慮尋找 Cn 的遞推公式。

 

  以下將 A 到 B 的那條斜線稱為“大斜線”,斜線上的點記為 A0(= A), A1, A2, ... , An(= B)。

 

  有如下推論:從 A 到 B,且不經過 A1, A2, ... , An-1 中任何一點的走法有 Cn-1 種。

 

證:從 A 到 B,且不經過 A1, A2, ... , An-1 中任何一點相當於從 D 走到 E 的 x≥y格路問題,故有 Cn-1 種走法。證畢。

 

  對於從 A 到 B 的任意一條路徑,記這條路徑中,第一次碰到大斜線的那個點為 F。則我們可以把 A 到 B 的所有路徑分成如下 n 類:

 

  F = A1
  F = A2
  ...
  F = An = B

 

  注意前面 F 的定義是第一次碰到大斜線,故 F 之前的路徑都沒碰到。易證對於任意一條 A 到 B 的路徑 L,它一定屬於某一個分類,而且不可能同時屬於兩個不同的分類。可見這些分類是互不相交的,而且正好完全覆蓋了 A 到 B 的所有路徑。

 

  於是我們可以分別計算每一類的數量。

 

  對 F = Ai,顯然 A 到 F 以及 F 到 B 可以看成 i 階和 n - i 階 x≥y格路問題,又由於 A 到 F 時沒有碰大斜線,故有 Ci-1 種,而 F 到 B 有 Cn-i 種,故由乘法原理,第 i 類有:Ci-1Cn-i 種。加起來得:

 

Cn = C0Cn-1 + C1Cn-2 + ... + Cn-1C0 ,   C0 = 1   (1)

  這樣我們就得到了 Cn 的遞推公式。

 

  下面再來看二叉樹計數問題。設 n 個結點的二叉樹有 Cn 種,除去根之後還有 n-1 個結點,給左子樹分 i 個結點,則右子樹有 n-1-i 個結點。顯然左右子樹分別化為兩個低階的二叉樹計數問題。於是令 i 從 0 取到 n-1,我們得到:

Cn = C0Cn-1 + C1Cn-2 + ... + Cn-1C0 ,   C0 = 1

  與上面的遞推公式一樣。故:二叉樹計數問題與逆波蘭式計數問題等價。

問題5(多邊形劃分):將 n+2 邊凸多邊形劃分成三角形,只能在兩個頂點之間連線,每個頂點看作是不同的。有多少種劃分方法?

例:n = 3 時,五邊形顯然有五種劃分,即從每個頂點連出兩條線。

 

  這個問題與前面幾個問題的等價性留給讀者自證。

 

  下面我們就來求解遞推關係 (1) 式。

問題6:已知:C0 = 1,Cn = C0Cn-1 + C1Cn-2 + ... + Cn-1C0。求:Cn。

解:(母函數法)

 

  將數列 {Cn} 作為形式冪級數的係數,構造出數列 {Cn} 的母函數:

 

    f(x) = C0 + C1x + C2x2 + ... + Cnxn + ...

 

  於是:

 

    f2(x) = (C0 + C1x + C2x2 + ... + Cnxn + ...) * (C0 + C1x + C2x2 + ... + Cnxn + ...)

     = C0C0 + (C0C1 + C1C0)x + (C0C2 + C1C1 + C2C0)x2 + ...

     = C1 + C2x + C3x2 + ... + Cn+1xn + ...

 

  將上式兩邊同乘以 x,再加 C0(= 1),得:

 

    1 + xf2(x) = C0 + C1x + C2x2 + ... + Cn+1xn+1 + ...

     = f(x)

 

  於是我們得到關於 f(x) 的方程:

 

    1 + xf2(x) = f(x)

 

  解之,得:

 

       (2)

 

  對上式作泰勒展開(準確說是形式冪級數展開,反正就那個東西):

 

  回顧泰勒公式:

 

    

 

  令 x = -4x,a = 1/2 代入,得:

 

    

 

  其通項可化為:

 

    

    

 

  由於 Cn > 0,故可知 (2) 式中應取負號。上面的通項取負後,再除以 2x,得到 f(x) 的通項:

 

    

 

  故:

 

    

    

  這個數在組合數學中被稱為卡塔蘭(Catalan)數

 

  由此我們看到,從一個看似簡單的逆波蘭式計數問題,可以挖掘出這麼多東西,其實寫程式枚舉所有合法的逆波蘭式是可以的,相當於枚舉所有滿足要求的 1,-1 序列。只不過對於一個湊24程式來說太複雜,就省了。

 

  下面是我的枚舉 1,-1 序列程式:

#include <stdbool.h><br />#include <stdio.h></p><p>/**<br /> * 根據當前的序列和 n,得到下一個序列。<br /> * 對這個函數來說,第一個序列是 1,-1 交錯狀態,<br /> * 最後一個序列是前面全是 1,後面全是 -1。<br /> *<br /> * @param ar 當前序列值,必須有 2n 項<br /> * @param n<br /> * @return true若下一個序列存在,false若當前已經是最後一個序列<br /> */<br />bool nextSeq(int ar[], int n)<br />{<br /> int sm1, s1;<br /> int i = 2 * n - 1;<br /> while (ar[i] == -1) // 尋找末尾連續的 -1<br /> {<br /> i--;<br /> }<br /> sm1 = 2 * n - i;<br /> while ((ar[i] == 1)&&(i >= 0)) // 尋找 -1 之後連續的 1<br /> {<br /> i--;<br /> }<br /> if (i < 0)<br /> {<br /> return false;<br /> }<br /> s1 = 2 * n - 1 - i - sm1;<br /> ar[i] = 1; // 將這個 -1 改成 1<br /> i++;<br /> int j;<br /> // 填充剩下的<br /> for (j = sm1 - s1; j > 0; j--)<br /> {<br /> ar[i] = -1;<br /> i++;<br /> }<br /> for (j = s1; j > 0; j--)<br /> {<br /> ar[i] = 1;<br /> ar[i + 1] = -1;<br /> i += 2;<br /> }<br /> return true;<br />}</p><p>int main(int argc, char *argv[])<br />{<br /> int n = 4;<br /> int ar[2 * n];<br /> int i;<br /> int count = 0;<br /> for (i = 0; i < n; i++)<br /> {<br /> ar[2 * i] = 1;<br /> ar[2 * i + 1] = -1;<br /> }<br /> do<br /> {<br /> for (i = 0; i < 2 * n; i++)<br /> {<br /> printf("%d ", ar[i]);<br /> }<br /> printf("/n");<br /> count++;<br /> } while(nextSeq(ar, n));<br /> printf("count: %d/n", count);<br /> return 0;<br />}<br />

  這個程式用 C 語言編寫(C 不是 C++ !),用到了部分 C99 特性,比如 bool 型,棧上的動態大小數組等,編譯器用的是 GCC 4.5。所以如果你用 VC++ 可能必須改代碼才能編譯通過,但用 codepad(http://codepad.org/)可以編譯通過並運行。codepad 的運行結果:http://codepad.org/3u8eLcar

 

  修改 main 函數中 n 的值可以輸出不同的 n 的值的結果。nextSeq() 函數根據當前的序列計算下一個序列,時間複雜度 O(n)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.