問題描述:棧是常用的一種數據結構,有n個元素在棧頂端一側等待進棧,棧頂端另一側是出棧序列。你已經知道棧的操作有兩種:push和pop,前者是將一個元素進棧,后者是將棧頂元素彈出。現在要使用這兩種操作,由一個操作序列可以得到一系列的輸出序列。請你編程求出對於給定的n,計算並輸出由操作數序列1,2,…,n,經過一系列操作可能得到的輸出序列總數。
分析:之前就有看過這種問題。就是火車進站問題,判斷序列是否合法,當時是用STL棧做的。這個題只需統計次數,那么,方法就十分簡便了,遞歸和動歸都可以實現,當然用棧模擬當然也OK。值得一提的是網上看到的一個分析方法(引自石家庄鐵道大學 劉莉等論文)
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點到任意一點C的路徑數目=從A點到D點(C的左鄰點)的路徑數目+從A點到E點(C的下鄰點)的路徑數目
其中,由於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]中車廂調度問題。
代碼:分為遞歸和這個論文的算法求解
1、遞歸版本
#include<iostream> using namespace std; int num,n; //sz表示當前棧大小,i是指向等待調度序列的指針 void calcuNum(int i,int sz){ if(i>n){//所有序列都已處理完畢 num++; return; } calcuNum(i+1,sz+1);//入棧 if(sz>0)//出棧 { calcuNum(i,sz-1); } } int main() { cin>>n; calcuNum(1,0); cout<<num; return 0; }
2、兩點路徑問題
#include<iostream> #include<stack> using namespace std; int num,n; //sz表示當前棧大小,i是指向等待調度序列的指針 void calcuNum(int i,int sz){ if(i>n){//所有序列都已處理完畢 num++; return; } calcuNum(i+1,sz+1);//入棧 if(sz>0)//出棧 { calcuNum(i,sz-1); } } void path(){ int s[n+1][n+1]; for(int i=0;i<=n;i++) s[0][i]=1; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(i < j) s[i][j] = s[i-1][j]+s[i][j-1]; if(i == j) s[i][j] = s[i-1][j]; } } num = s[n][n]; } int main() { cin>>n; path(); cout<<num; return 0; }
某列車調度站的鐵道聯接結構如Figure 1所示。
其中,A為入口,B為出口,S為中轉盲端。所有鐵道均為單軌單向式:列車行駛的方向只能是從A到S,再從S到B;另外,不允許超車。因為車廂可在S中駐留,所以它們從B端駛出的次序,可能與從A端駛入的次序不同。不過S的容量有限,同時駐留的車廂不得超過m節。
設某列車由編號依次為{1, 2, ..., n}的n節車廂組成。調度員希望知道,按照以上交通規則,這些車廂能否以{a1, a2, ..., an}的次序,重新排列后從B端駛出。如果可行,應該以怎樣的次序操作?
輸入
共兩行。
第一行為兩個整數n,m。
第二行為以空格分隔的n個整數,保證為{1, 2, ..., n}的一個排列,表示待判斷可行性的駛出序列{a1,a2,...,an}。
輸出
若駛出序列可行,則輸出操作序列,其中push表示車廂從A進入S,pop表示車廂從S進入B,每個操作占一行。
若不可行,則輸出No
下面,是之前我在UVa上做的題解法(不完善)
#include <iostream> #include<stack> using namespace std; int const MAX = 1000; int testOrder(int n) { stack<int> train; int ch[MAX]; int curA=1,curB=1; for(int i =1; i<=n; i++){ cin>>ch[i]; if(ch[i] ==0){ return 0; } } while(curB<=n){ if(ch[curB] == curA){ curB++; curA++; }else if(!train.empty() && ch[curB] == train.top()){ train.pop(); curB++; }else if(curA<=n){ train.push(curA++); }else{ cout<<"No"<<endl; return 1; } } cout<<"Yes"<<endl; return 1; } int main() { int n; cin>>n; do{ while(testOrder(n)==1); cin>>n; if(n!=0) cout<<endl; }while(n != 0); return 0; }