n 個元素順序入棧,則可能的出棧序列有多少種?

有關堆棧和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)的時候又計算了一遍。當然可以通過動態規劃的思想設置一個二維數組來記錄計算結果,可是太消耗空間。
如果直觀的方法就能很高效地解決問題的話,就不會有那麼多人去從數學上求解了。

遞歸的致命缺點,也是優點就是把複雜的計算留給機器, 遞歸往往能迅速簡潔的思維求出問題的解, 也許得到的算法是低效的.所以遞歸應該是一種懶人算法. 而"從數學上求解"應當是指從正向考慮問題的解, 遞歸是逆向求解

 

轉自:http://blog.sina.com.cn/s/blog_66f4dd01010129a7.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章