求棧的出棧方式的個數和打印出棧順序
無重複序列入棧的出棧方式個數
首先,出棧方式的個數為h(n)=C(2n,n)/(n+1) (n=1,2,3,…)
例如1、2、3這三個數字,入棧並出棧共有5種方式,分別為:321、312、231、213、123。
再例如,對於1、2、3、4這四個數字,4個元素的全排列共有24種,棧要求符合後進先出,按此衡量排除後即得:
1234√ 1243√ 1324√ 1342√ 1423× 1432√
2134√ 2143√ 2314√ 2341√ 2413× 2431√
3124× 3142× 3214√ 3241√ 3412× 3421√
4123× 4132× 4213× 4231× 4312× 4321√
入棧並出棧共有14種可能。
那麼對於長度為n的無重複序列中所有的出棧方式有哪些呢?
1個元素進棧,有1種出棧順序;
2個元素進棧,有2種出棧順序;
3個元素進棧,有5種出棧順序
我們把n個元素的出棧個數的記為f(n), 那麼對於1,2,3, 我們很容易得出:
f(1) = 1 //即 1
f(2) = 2 //即 12、21
f(3) = 5 //即 123、132、213、321、231
然後我們來考慮f(4), 我們給4個元素編號為a,b,c,d,輸出結果第一個出棧的位置為1號位置,第二個出棧的位置為2號位置, 那麼考慮:元素a只可能出現在1號位置,2號位置,3號位置和4號位置(很容易理解,一共就4個位置,比如出棧結果為abcd,元素a就在1號位置)。
分析:
1) 如果元素a在1號位置,那麼只可能a進棧之後馬上出棧,此時還剩元素b、c、d等待操作,就是子問題f(3);
2) 如果元素a在2號位置,那麼一定有一個元素比a先出棧,即有f(1)種可能順序(只能是b),還剩c、d,即f(2), 根據乘法原理,無論cd什麼順序,元素a在2號位置的順序只有f(1)種可能順序,那麼一共的順序個數為f(1) * f(2);
3) 如果元素a在3號位置,那麼一定有兩個元素比1先出棧,即有f(2)種可能順序(只能是b、c),還剩d,即f(1),根據乘法原理,一共的順序個數為f(2) * f(1);
4) 如果元素a在4號位置,那麼一定是a先進棧,最後出棧,那麼元素b、c、d的出棧順序即是此小問題的解,即 f(3);
結合所有情況,即f(4) = f(3) + f(2) * f(1) + f(1) * f(2) + f(3);
為了規整化,我們定義f(0) = 1;於是f(4)可以重新寫為:
f(4) = f(0)f(3) + f(1)*f(2) + f(2) f(1) + f(3)*f(0)
然後我們推廣到n,推廣思路和n=4時完全一樣,於是我們可以得到:
f(n) = f(0)*f(n-1) + f(1)*f(n-2) + … + f(n-1)*f(0)
即
f(n)=sum(0,n-1,f(i)*f(n-1-i) )
根據此遞迴公式,相應的動態規劃程式碼如下(來自:
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
int n;
cin >> n;
int* arr = new int[n+1];
memset(arr, 0, sizeof(int)*(n+1));
arr[0] = 1;
arr[1] = 1;
//遞推關係式
for (int i=2; i<=n; ++i)
{
for (int j=0; j<i; ++j)
{
arr[i] += arr[j] * arr[i-1-j];
}
}
cout << arr[n] << endl;
delete[] arr;
return 0;
}
由於遞迴公式的時間複雜度仍為O(n2),通項公式則更佳,因此,我還摘錄了通項公式法的分析(來自:http://www.cnblogs.com/jiayouwyhit/p/3222973.html)
對於每一個數來說,必須進棧一次、出棧一次。我們把該數進棧設為狀態‘1’,出棧設為狀態‘0’。n個數的所有狀態對應n個1和n個0組成的2n位二進位制數。由於等待入棧的運算元按照1‥n的順序排列、入棧的運算元b大於等於出棧的運算元a(a≤b),因此輸出序列的總數目=由左而右掃描由n個1和n個0組成的2n位二進位制數,1的累計數不小於0的累計數的方案種數。
(如果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)。其中,n為節點的個數
此時,通項公式得到,其成為卡塔蘭數。
打印出棧順序/打印出棧方式
為了設計打印出出棧順序的演算法,我們可以用佇列(queue)來模擬輸入,佇列的輸出則按照原先序列的順序。使用一個棧(stack)來模擬入棧和出棧,結果儲存在另外一個佇列(queue)中。
現在的問題來了,怎麼樣可以實現所有的出棧入棧操作。
首先來看看出棧和入棧是怎麼回事,對於123這個序列,1先入棧之後有兩種選擇,1出棧和不出棧(2入棧),而若1不出棧(2已經入棧)之後,在2出棧之前1則不能先行出棧,故對於1我們只需要考慮其在2入棧之前出棧的情況,若1在棧內時2入棧,則1與2只能看成一個整體。
這樣就可以用遞迴的方式求解,以下為演算法描述,其中input表示入棧元素的順序,local表示模擬的棧,output表示在已經出棧的部分解。
虛擬碼如下:
Solution(input,local,output)
If(input.empty)
/*由於input佇列已經沒有內容,此時沒有入棧元素的影響,local棧和output佇列的內容就是最後的結果*/
If(local.empty() )
輸出output佇列的內容
Else
While(local棧不空)
將local的棧頂元素出棧,放進output佇列之中
Solution(input,local,output)
end if
Else
/*當有入棧元素時,出棧元素順序會受到入棧元素和棧頂元素是否出棧的影響*/
If(local.empty() )
從input佇列中出佇列一個元素並放入local之中
Solution(input,local,output)
end if
Else
/*此時有兩種可能,可以是local棧的棧頂元素出棧,也可以是不出棧並從input佇列中取一個元素放進去,先模擬出棧,再把棧內元素放回去*/
將local的棧頂元素出棧,暫存,並放入output佇列之中
Solution(input,local,output)
將剛剛出棧的local的棧頂元素放回local棧之中
將剛剛放入output佇列之中的元素拿出來
從input佇列中出佇列一個元素並放入local之中
Solution(input,local,output)
/*兩種方法,一種是這裡的利用output和local進行分叉搜尋,另一種搜尋方法是從input中取出元素壓棧(此時棧頂元素不出棧)時查找出棧順序,然後把壓入棧的元素彈出來放回input之中,然後local棧頂元素出棧,去搜索此時的出棧順序。*/
end if
End Solution
其基本思想為對於中間棧的每一個時刻拍照,都遞迴其後續的所有可能,由於在遞迴返回的時候還需要遞迴前的資訊,所以每次遞迴都是新建資料結構而儲存當前時刻的狀態。若輸入佇列已經為空,則中間棧只有一種出棧方式,中間棧也為空時遞迴結束。
詳細程式碼如下:
#include <iostream>
#include <stack>
#include <deque>
#include <queue>
using namespace std;
void dfs(queue<int> input, stack<int> local, deque<int> output) {
if (input.empty())
{
if (local.empty())
{
cout << endl;
int temp;
while (!output.empty())
{
temp = output.back();
output.pop_back();
cout << temp << " ";
}
cout << endl;
}
else
{
while (!local.empty())
{
output.push_front(local.top());
local.pop();
}
dfs(input, local, output);
}
}
else
{
if (!local.empty())
{
int temp = local.top();
local.pop();
output.push_front(temp);
dfs(input, local, output);
local.push(temp);
output.pop_front();
temp = input.front();
input.pop();
local.push(temp);
dfs(input, local, output);
}
else
{
int temp = input.front();
input.pop();
local.push(temp);
dfs(input, local, output);
}
}
}
int main() {
queue<int> input_iterator_tag;
for (int i = 1; i < 5; i++)
{
input_iterator_tag.push(i);
}
dfs(input_iterator_tag, stack<int>{}, deque<int>{});
return 0;
}
類似的問題
(1)買票找零
有2n個人排成一行進入劇場。入場費5元。其中只有n個人有一張5元鈔票,另外n人只有10元鈔票,劇院無其它鈔票,問有多少中方法使得只要有10元的人買票,售票處就有5元的鈔票找零?(將持5元者到達視作將5元入棧,持10元者到達視作使棧中某5元出棧)
(2)一個有n個1和n個-1組成的字串,且前k個數的和均不小於0,那這種字串的總數為多少?
(3)飯後,姐姐洗碗,妹妹把姐姐洗過的碗一個一個地放進碗櫥摞成一摞。一共有n個不同的碗,洗前也是摞成一摞的,也許因為小妹貪玩而使碗拿進碗櫥不及時,姐姐則把洗過的碗摞在旁邊,問:小妹摞起的碗有多少種可能的方式?
最終結果:C(2n,n)-C(2n,n+1)
有機會再補充吧,最近刷題這麼猛,還是找不到工作,人艱不拆。
參考文獻:
https://zhidao.baidu.com/question/198485664882428485.html
http://www.cnblogs.com/jiayouwyhit/p/3222973.html