1. 程式人生 > >卡特蘭數---n 個元素順序入棧,則可能的出棧序列有多少種

卡特蘭數---n 個元素順序入棧,則可能的出棧序列有多少種

題目:N個數依次入棧,出棧順序有多少種? 首先介紹一下卡特蘭數:卡特蘭數前幾項為 : 1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, ... 令h(0)=1,h(1)=1,卡特蘭數滿足遞推式:h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)h(0) (n>=2) 例如:h(2)=h(0)*h(1)+h(1)*h(0)=1*1+1*1=2 ,h(3)=h(0)*h(2)+h(1)*h(1)+h(2)*h(0)=1*2+1*1+2*1=5; 另類遞推式:h(n)=h(n-1)*(4*n-2)/(n+1); 遞推關係的解為:h(n)=C(2n,n)/(n+1) (n=1,2,3,...); 遞推關係的另類解為:h(n)=C(2n,n)-C(2n,n+1)(n=1,2,3,...); 常規分析 首先,我們設f(n)=序列個數為n的出棧序列種數。同時,我們假定第一個出棧的序數是k。第一個出棧的序數k將1~n的序列分成兩個序列,其中一個是1~k-1,序列個數為k-1,另外一個是k+1~n,序列個數是n-k。此時,我們若把k視為確定一個序數,那麼根據乘法原理,f(n)的問題就等價於——序列個數為k-1的出棧序列種數乘以序列個數為n - k的出棧序列種數,即選擇k這個序數的f(n)=f(k-1)×f(n-k)。而k可以選1到n,所以再根據加法原理,將k取不同值的序列種數相加,得到的總序列種數為:f(n)=f(0)f(n-1)+f(1)f(n-2)+……+f(n-1)f(0)。看到此處,再看看卡特蘭數的遞推式,答案不言而喻,即為f(n)=h(n)= C(2n,n)/(n+1)= C(2n,n)-C(2n,n+1)(n=1,2,3,……)。最後,令f(0)=1,f(1)=1。 非常規分析 對於每一個數來說,必須進棧一次、出棧一次。我們把進棧設為狀態‘1’,出棧設為狀態‘0’。n個數的所有狀態對應n個1和n個0組成的2n位二進位制數。由於等待入棧的運算元按照1‥n的順序排列、入棧的運算元b大於等於出棧的運算元a(a≤b),因此輸出序列的總數目=由左而右掃描由n個1和n個0組成的2n位二進位制數,1的累計數不小於0的累計數的方案種數。 在2n位二進位制數中填入n個1的方案數為C(2n,n),不填1的其餘n位自動填0。從中減去不符合要求(由左而右掃描,0的累計數大於1的累計數)的方案數即為所求。 不符合要求的數的特徵是由左而右掃描時,必然在某一奇數位2m+1位上首先出現m+1個0的累計數和m個1的累計數,此後的2(n-m)-1位上有n-m個 1和n-m-1個0。如若把後面這2(n-m)-1位上的0和1互換,使之成為n-m個0和n-m-1個1,結果得1個由n+1個0和n-1個1組成的2n位數,即一個不合要求的數對應於一個由n+1個0和n-1個1組成的排列。 反過來,任何一個由n+1個0和n-1個1組成的2n位二進位制數,由於0的個數多2個,2n為偶數,故必在某一個奇數位上出現0的累計數超過1的累計數。同樣在後面部分0和1互換,使之成為由n個0和n個1組成的2n位數,即n+1個0和n-1個1組成的2n位數必對應一個不符合要求的數。 因而不合要求的2n位數與n+1個0,n-1個1組成的排列一一對應。 顯然,不符合要求的方案數為C(2n,n+1)。由此得出輸出序列的總數目=C(2n,n)-C(2n,n+1)=C(2n,n)/(n+1)=h(n+1)。 類似問題 買票找零 有2n個人排成一行進入劇場。入場費5元。其中只有n個人有一張5元鈔票,另外n人只有10元鈔票,劇院無其它鈔票,問有多少中方法使得只要有10元的人買票,售票處就有5元的鈔票找零?(將持5元者到達視作將5元入棧,持10元者到達視作使棧中某5元出棧) 最終結果:C(2n,n)-C(2n,n+1) 有關堆疊和Catalan數的思考


*
* *
* * *
* * * *
* * * * *
形如這樣的直角三角形網格,從左上角開始,只能向右走和向下走,問總共有多少種走法?

問題的由來:編號為 1 到 n 的 n 個元素,順序的進入一個棧,則可能的出棧序列有多少種?

對問題的轉化與思考:n 個元素進棧和出棧,總共要經歷 n 次進棧和 n 次出棧。這就相當於對這 2n 步操作進行排列。

一 個模型:一個 n*n 的正方形網格,從左上角頂點到右下角頂點,只能向右走和向下走。問共有多少種走法。如果將向右走對應上述問題的進棧,向下走對應上述問題的出棧,那麼,可以視此模型為對上述問題的具體描述。而解決此問題,只要在總共從左上角到右下角的2n步中,選定向右走的步數,即共有C(n 2n)中走法。

但是存在一個問題,如果走法越過了對角線,那麼對應到上述問題是出棧數比入棧數多,這是不符合實際的。

對以上模型進行處理,對角線將以上正方形網格分成兩部分,只留下包含對角線在內的下半部分,那麼就不會出現越過對角線的問題。而這問題就是開始提出的問題。
-------------------------------------------------------

問題等價於:n個1和n個0組成一2n位的2進位制數,要求從左到右掃描,1的累計數不小於0的累計數,試求滿足這條件的數有多少?
解答: 設P2n為這樣所得的數的個數。在2n位上填入n個1的方案數為 C(n 2n)
不填1的其餘n位自動填以數0。從C(n 2n)中減去不符合要求的方案數即為所求。
不合要求的數指的是從左而右掃描,出現0的累計數超過1的累計數的數。

不合要求的數的特徵是從左而右掃描時,必然在某一奇數2m+1位上首先出現m+1個的累計數,和m個1的累計數。
此後的2(n-m)-1位上有n-m個1,n-m-1個0。如若把後面這部分2(n-m)-1位,0與1交換,使之成為n-m個0,n-m-1個1,結果得 1個由n+1個0和n-1個1組成的2n位數,即一個不合要求的數對應於一個由n-1個0和n+1個1組成的一個排列。

反過來,任何一個由n+1個0,n-1個1組成的2n位數,由於0的個數多2個,2n是偶數,故必在某一個奇數位上出現0的累計數超過1的累計數。同樣在後面的部分,令0 和1互換,使之成為由n個0和n個1組成的2n位數。即n+1個0和n-1個1組成的2n位數,必對應於一個不合要求的數。

用上述方法建立了由n+1個0和n-1個1組成的2n位數,與由n個0和n個1組成的2n位數中從左向右掃描出現0的累計數超過1的累計數的數一一對應。

例如 10100101

是由4個0和4個1組成的8位2進位制數。但從左而右掃描在第5位(顯示為紅色)出現0的累計數3超過1的累計數2,它對應於由3個1,5個0組成的10100010。

反過來 10100010

對應於 10100101

因而不合要求的2n位數與n+1個0,n-1個1組成的排列一一對應,故有

P2n = C(n 2n)— C(n+1 2n)

這個結果是一個“卡塔蘭數”Catalan,在組合數學中有介紹,可以參閱有關資料。

-----------------------------------------------------

是否可以用一種更合乎思維習慣的方式解決這個問題呢
可以把這個問題描述為一個二元組,(n, 0) 表示有n個元素等待進棧, 0 個元素已進棧, 這相當於問題最初的狀況. 接著問題轉化為(n-1,1). 可以這麼說(n,0) = (n-1,1). 而對於(n-1,1)則相當於(n-1,0)+(n-2,2).
(n-1,0)表示棧中的一個元素出棧, (n-2, 2)表示又有一個元素入棧.
把問題一般話,則(n,m)的排列問題可以轉化為(n,m-1)+(n-1,m+1) 此時m>=1, 因為必須棧中有元素才可以出棧.當m=0則(n,0)的問題只能轉化為(n-1,1). 當問題為(0, m)時得到遞迴邊界,這個問題的解是隻有一種排列.

程式如下:

#include <stdio.h>

#define ELEMNUM 6;


int getPermuStack(int n, int m)
{
if(n == 0)//遞迴邊界
return 1;
if(m == 0)//(n,0)問題的處理
return getPermuStack(n-1, 1);
return getPermuStack(n, m-1) + getPermuStack(n-1, m+1);
}


int main()
{
printf("The total count of stackout permutation is %d.", getPermuStack(6, 0));
return 0;
}

執行結果:
The total count of stackout permutation is 132.

-----------------------------------------------------

上面方法是可行的,但在實際程式設計中最好不要用遞迴,這樣如果遞迴次數一多就容易造成棧溢位。你可以試下用較大的引數來呼叫你的函式,會造成runtime error的。
而 且純粹的遞迴會造成大量的重複計算:比如,你在計算getPermuStack(5, 5)的時候計算了getPermuStack(5, 4),然後在計算getPermuStack(5,3)的時候又計算了一遍。當然可以通過動態規劃的思想設定一個二維陣列來記錄計算結果,可是太消耗空間。
如果直觀的方法就能很高效地解決問題的話,就不會有那麼多人去從數學上求解了。