N個元素的進出棧總數-方法轉換-動態規劃
其實此題還可以用動歸方法解決,f[i,j],i表示入棧的個數,j表示出棧的個數,那f[i,j]就表示入棧i個數中出j個數的,但是此題要注意的是出棧數不能大於入棧數,那動歸方程該如何推導,再次謝謝一位某位具有探索精神的大神為我們做了細緻的研究現在我把它的論文貼上如下:
1引 言
在實際應用和資料結構課程的教學中,棧作為一種基本結構非常重要[1][3][4][6]。已知給定序列,求出棧序列的數目、求所有出棧的序列、以及判斷某個序列是否為合法的出棧序列[5][7],這類問題經常出現。在[3]中,對出棧序列的計數問題給出了介紹性的說明,由於結果的證明需要用到生成函式,[3]也只是直接給出了結論。
2問題分析
兩點之間路徑計數的問題
問題:假設A、B兩點之間所有道路如圖1中的方形網格線(5×5),規定從A到B只能夠向右或向上移動,求A點到B點有幾條路。
圖1 兩點之間路徑計數問題的路徑圖
分析:因為規定從A到B只能夠向右或向上移動,因此任意一點只能從該點的左鄰點或下鄰點到達,例如,任意一點C點只能從D點或E點到達。因此,從A點到C點的路只能由這兩部分組成:①從A點到D點,再從D點到C點;②從A點到E點,再從E點到C點。
結論1:A點到任意一點
其中,由於A點正上方的點沒有左鄰點,而且問題中已規定從A到B只能夠向右或向上運動,所以A點到A點正上方點的路徑數目為1。同理,A點到A點正右方點的路徑數目為1。
根據結論1,將A點到任意一點的路徑數目求出,如圖1中網格線交點處的數字所示。
棧的操作與兩點之間路徑計數問題的操作的比較
棧的操作有兩種:入棧、出棧。其中需要注意的問題有三個:①所有節點入棧之後只能出棧;②棧空時只能入棧;③其它情況下入棧、出棧任意執行。
兩點之間路徑計數問題的操作有兩種:向右移動、向上移動。其中需要注意的問題有三個:①移到最右邊後只能向上移;②移到最上邊只能向右移;③其它情況下上移、右移任意執行。
由以上分析可見,棧的操作與兩點之間路徑計數問題的操作有很大的相似性,不妨將入棧和右移相關聯,出棧和上移相關聯。但是,這樣關聯之後,由於兩個問題並不等價(例如,圖1中的D點,按照棧的操作是不可到達的),所以需要對圖1中的所有點進一步分析。
對操作關聯後圖1中點的分析
首先,在圖1中新增A點到B點的對角虛線。這條虛線將所有的點分成三類:①虛線上的點;②虛線左上方的點;③虛線右下方的點。
其次,按照兩點之間路徑計數問題規定的操作容易得出:①從A點移到虛線上每一個點時,所執行的右移操作次數和上移操作次數相等;②從A點移到虛線左上方每一個點時,所執行的右移操作次數小於上移操作次數;③從A點移到虛線右下方每一個點時,所執行的右移操作次數大於上移操作次數。
再次,由於棧操作過程中的任意時刻必須有:入棧操作次數≥出棧操作次數(取等號時棧空)。
很明顯,圖1中虛線左上方的點按照棧的操作是不可到達的,虛線上的點恰好是棧空時的狀態,虛線右下方的點按照棧的操作都可以到達,所以考慮修改圖1中的路徑圖。
改進後的路徑圖及規則
將圖1中虛線左上方的點去掉後如圖2所示(5×5方形網格線的下三角)。
圖2 改造後的的路徑圖
規定:從A到B只能夠向右或向上移動,右移為入棧操作,上移為出棧操作。
根據2.3的分析可得結論2:
①從A點到A點正右方的點的路徑數目 = 1;
②從A點到每一行最左的點(考慮B點,不考慮A點)的路徑數目 =從A點到該點的下鄰點的路徑數目;
③從A點到其它任意一點C的路徑數目=從A點到D點(C的左鄰點)的路徑數目+從A點到E點(C的下鄰點)的路徑數目;
④按照棧的操作從A點開始到B點,圖2中的所有點都是可到達的;
⑤4個節點的入棧、出棧操作完全包含在圖2中;
⑥將⑤擴充套件得:N個節點的出棧、入棧操作完全包含在(N+1)×(N+1)方形網格線的下三角中。
在此僅對結論2第⑤點作一些說明:首先A點和對角線上的其它點表示棧空,只能入棧(右移);其次,移到最右的豎邊時所有的元素都已經入棧,只能出棧(上移);再次,B點為最終狀態,不能入棧也不能出棧;最後,其它的點可以任意入棧(右移)、出棧(上移)。所以4個節點的入棧、出棧操作完全包含在圖2中。
從結論2中可以看到棧的操作與兩點之間路徑計數問題的操作在圖2中是等價的。根據結論2,將A點到任意一點的路徑數目求出,如圖2中網格線交點處的數字所示,圖2中虛線箭頭表示了執行結論2第①點,圖2中實線箭頭表示了執行結論2第③點。
結論2推廣
將結論2加以推廣得結論3:對於如圖2的形式((N+1)×(N+1)方形網格線的下三角),規定從A到B只能夠向右或向上移動,右移為入棧操作,上移為出棧操作,所求A點到B點的路徑數目就是N個節點出棧序列的數目,並且從A點到B點的每一條路都代表一種出棧序列。
3設計實現
求N個節點出棧序列數目的演算法在具體實現時,採用由下向上逐行處理,每一行從左至右逐點處理的方法。此外,在 計算當前行的值時,只需要使用上一行的值;在計算各行中的每一個值時左鄰點的值為陣列中當前元素的前一個元素,下鄰點為陣列中當前元素,把陣列中當前元素 的前一個元素加到當前元素上就求出了當前點的值,所以在具體實現時,只使用一個數組來儲存當前行的值即可。由於最後一行只有一個數,也是該行的第一個數, 根據結論2第③點可知該數在倒數第二行中已經計算出來,所以該行不用計算,直接取上一行計算結果中的最後一個數即可。
4結論
該方法簡單方便,不需要記憶任何公式[3],特別適合沒有組合數學基礎的人員。另外,根據圖2還可以設計演算法將入棧、出棧的操作序列求出來,這樣就可以得到所有的出棧序列。同時根據圖2也可以判斷某個序列是否為合法的出棧序列,可以解決[5][7]中車廂排程問題。
為表感謝,特把該作者文章標紅顯示,通過該大神深入淺出的講解,我相信只要不是比我還笨的人都能理解了,大神為了方便講解所以把向右定義為入棧,向上定義為出棧,但為了方便書寫程式碼,我把向下定義為出棧,向右定義為入棧,那麼動歸方程就推導如下f[i,j]:=f[i-1,j]+f[i,j-1], 程式碼如下:
問題分析:
由題目可知,可能的動作只有進棧和出棧,且出棧次數<=入棧次數,因此可修改該問題的表現。假設N=3,建立一個(N+1)×(N+1)的網格,如下圖所示(以線段交叉點為網格點):
從網格左下角出發,規定向上走一步為一次入棧操作,向右走一步為一次出棧操作,由於出棧次數<=入棧次數,因此可行範圍限定在對角線及其以上區域。設N(m,n)是經過n次入棧,m次出棧後可能的結果數量,由圖可知,入棧n次,出棧m次,是由一次出棧或一次出棧形成的。因此N(m,n)=N(m-1,n)+ N(m,n-1),其中N(0,0)=1,即初始狀態為1;左側邊界表示只能有入棧操作,因此N(0,n)=N(0,n-1)。圖中×表示不可能出現的情況,因此也可以認為是0。這樣N(3,3)便為元素為3的情況下所有可能的出棧序列的次數。
該問題求解需要一個(N+1)×(N+1)的網格進行輔助運算,所以空間複雜度為O(n2),由於是從前往後的遞推求解,因此可用兩層迴圈巢狀的方式計算出網格內的所有點,因此時間複雜度為O(n2)。- #include <stdio.h>
- #include <stdlib.h>
- int D[13][13];//預設棧最大為13,如有需要請自行修改
- void Stack(int M)
- {
- int i,j;
- for (i=0;i<=M;i++)
- {
- for (j=0;j<=M;j++)
- {
- D[i][j]=0;
- }
- }
- for (i=0;i<=M;i++)
- {
- D[i][0]=1;
- }
- for (i=1;i<=M;i++)
- {
- for (j=1;j<=M;j++)
- {
- if (i>=j)
- {
- D[i][j]=D[i-1][j]+D[i][j-1];
- }
- }
- }
- }
- void main()
- {
- int M;
- scanf("%d",&M);
- Stack(M);
- printf("%d", D[M][M]);
- }
棧是一種常見的資料結構,有許多關於棧的問題,其中之一就是統計元素可能的出棧序列。具體說,就是給定n個元素,依次通過一個棧,求可能的出棧序列的個數。
如果我們用直接模擬的方法,當n較大時會很費時間;另一種方法是利用組合數學求出棧序列個數,得到公式
下面我們來看一種圖形化的方法證明這個等式,很容易理解的。
我們把對n個元素的n次進棧和n次出棧理解為在一個n * n的方格中向右走n次(代表進棧),向上走n次(代表出棧)。由於出棧次數不能大於進棧次數,我們可以得到這樣一個方格:
每次沿著實線走,所以,只要求出從方格左下角到右上角路徑的個數即可。我們把表格補全,考慮每一條不合法的路徑,如
在這條路徑上,必然有一個地方連續兩次向上,即從圖上藍點處開始,而且這個點必然在如圖所示的綠線上。我們以這個點為起點,把到左上角整條路經取反,也就是對稱上去,得到一條新路徑,但是超出了表格。我們知道,這條路徑包括n + 1次向上和n – 1次向下,也就是在一個(n + 1) * (n - 1)的方格中。由此我們知道,一條不合法路徑必然對應一個(n
+ 1) * (n - 1)方格中的路徑。同樣地,對於(n + 1) * (n - 1)方格中任意一條路徑,以這條路徑與綠線的第一個交點為起點到方格的右上方全部取反,即可得到一個在n * n方格中的不合法路徑。
注意資料溢位問題。適當在做乘法的過程中做下除法
- #include<iostream>
- usingnamespace std;
- longlongint f(int n)
- {
- if (n == 0 || n == 1)
- {
- return 1;
- }
- return n*f(n - 1);
- }
- longlongint fsum(int start, int end)
- {
- longlongint sum = 1;
- for (int i = start; i >= end; i--)
- {
- sum *= i;
- }
- return sum;
- }
- int main()
- {
- int n;
- longlongint s;
- while (cin >> n)
- {
- longlong s1, s2;
- s1 = fsum(n * 2, n + 2) / f(n - 1);
- s2 = fsum(n * 2, n + 1) / f(n);
- s = s2 - s1;
- cout << s << endl;
- }
- return 0;
- }