棧:裝水的杯子(四)列車排程問題
(四)列車排程問題
子淵暑假和爸爸媽媽一起回了一趟湖南老家,老家坐落在湘南丘陵的一個小山包上,交通非常不便,從寧波出發先要坐二十四小時火車,再坐三個多小時的汽車,最後步行兩小時才到家。
在火車站候車的時候,正好能夠看見窗外的V字形站臺,見子淵好奇地盯著站臺看,爸爸心裡有了一個主意。“子淵啊,爸爸有一個關於列車排程的問題,不知道你感不感興趣?”
“列車排程?你是說我們今天要坐的這種列車嗎?”
“是啊,鐵路進行列車排程時, 常把站臺設計成棧式結構的V字形站臺,就像外面的那種(如右圖所示)。那麼現在我來考考你:
(1) 設有編號為1,2,3,4的四輛列車, 順序開入棧式結構的站臺, 則可能的出棧序列有多少種
(2) 若進站的四輛列車順序如上所述, 那麼是否能夠得到1423,2413,3412和4312。的出站序列?為什麼?
子淵從包裡拿出紙筆計算起來,不一會紙上就寫滿了密密麻麻的答案:
可能的出棧序列:1234,1243,1324,1342,1432,2134,2143,2314,2341,2431, 3214,3241,3421,4321。
不可能的出棧序列:1423,2413,3124,3142,3412,4123,4132,4213,4231,4312。
如果是n輛列車,可能的出棧序列會有多少種?需要先將n取較小值時的可能序列數量計算出來,觀察是否有規律,設n輛列車可能的出棧序列數量為
F(1) = 1;
F(2) = 2;
F(3) = 5;
F(4) = 14;
子淵看來看去也找不出規律,只好做罷。
再來看看為什麼1423,2413,3412和4312等序列是不可能出現的,子淵似乎發現了些什麼:
在1423序列中,我們觀察子序列423,發現2夾在4和3之間,即最大的數先出棧,最小的數中間出棧,中間的數最後出棧——這無論如何不可能出現,因為它違反棧“後進先出”的特點;同樣2413序列中1夾在4和3之間,3412序列中1夾在4和2之間,4312序列中1夾在3和2之間,這都違反LOFI規律。
其他的不可能序列也可以按照同樣的方法判斷出來,但是由於知識結構的原因,雖然子淵能夠看到這些,卻不能從數學的角度加以歸納,也不能做出嚴格的數學證明。聰明的讀者,如果你感興趣的話,本文的後面有一些練習,其中練習
子淵把自己思索的結果告訴了爸爸,並請爸爸解決自己的疑問。
爸爸首先對子淵獨立思考的精神和思考問題的方法都給予了表揚,然後告訴他:
要求n輛列車可能的出棧序列總量,我們可以採用遞迴分治的思想(關於遞迴分治的詳細內容,以後我會在系列文章中介紹)。我們把過程分成兩步,根據組合數學計數原理中的乘法原理,總的可能性數量等於第一步的可能性數量和第二步的可能性數量之乘積。
我們以序號為n的列車為界,將列車分為兩部分,一部分是在第n輛列車之前出棧的列車,另一部分是在第n輛列車之後出棧的列車。設在n號列車之前出棧的列車數量為i(0<=i<n),則在n號列車之後出棧的列車數量為(n – 1 – i)。前i輛列車出棧的可能性數量有F(i),後(n – 1 – i)輛列車出棧的可能性數量有F(n – 1 – i),所以總的數量為F(i)* F(n – 1 – i)。
因此我們可以得到一個遞迴公式:F(n) = F(0)*F(n-1) + F(1)*F(n-2) + F(2)*F(n-3) + ... + F(n-1)*F(n-n);(其中n>=1,F(0) = 1)。
我們把資料代人進行檢驗:
F(1) = F(0)*F(1-1) = 1;
F(2) = F(0)*F(2-1) + F(1)*F(2-2) = 1 + 1 = 2;
F(3) = F(0)*F(3-1) + F(1)*F(3-2) + F(2)*F(3-3) = 2 + 1 + 2 = 5;
F(4) = F(0)*F(4-1) + F(1)*F(4-2) + F(2)*F(4-3) + F(3)*F(4-4)= 5 + 4 + 4 + 5 = 14;
完全正確!
其實這與史上非常經典的數列——Catalan(卡特蘭)數極其相似——就是n的初值少1。
Catalan數的遞迴公式是h(n) = h(1)*h(n-1) + h(2)*h(n-2) + …… + h(n-1)*h(1) (其中n>=2,h(1) = 1)。
後來又有人得到一個另類的遞迴公式:h(n) = ((4*n – 2) / (n + 1)) * h(n-1)(其中n>=2,h(1) = 1)。
根據遞迴公式我們可以得到數列的通項公式:
h(n) = C(2*n, n) / (n + 1) = (2*n)! / (n! * n! *(n + 1)) (其中n>=1)。
關於由遞迴公式推出數列通項公式的過程我們就不深究了,這裡要用到生成函式等相關內容,感興趣的話你可以自己去搜索有關Catalan數的更多知識。
至於第2個問題,我們可以用一個數學引理來歸納子淵發現的規律:
引理:以1…n順序壓棧的出棧序列為p1, p2,…, pn,對任意的pi而言,pi+1,…, pn中比pi小的數必須是按逆序排列的,即對任意的i < j < k而言,若pi > pj且pi > pk,則必有pj > pk。
關於引理的證明我把它作為本文的一個課後練習(練習4),有興趣的讀者可以自行證明。此外,練習5還要求根據該引理設計一個演算法來判斷某個輸出序列是否正確。
這些問題我們暫時都先不去管,接下來看看爸爸又給子淵出了什麼難題。
“子淵啊,剛才爸爸給你介紹的那些東西都是比較高深的數學知識,我猜你不太搞得懂,現在爸爸給你出一個簡單點的題目,可以直接利用我們前面所學的棧來解決。”
“剛才我確實聽得雲裡霧裡的。什麼問題能夠直接用棧解決啊?真是太好了!”
“下面的問題需要模擬各個元素的入棧和出棧過程來解決:
若輸入序列1, 2, 3, …, n,請模擬各元素的入棧和出棧過程,判斷一個輸出序列是否正確。設計一個演算法來實現該功能,子函式介面為:
FUNCTION TrueList(inList, outList : List; len : integer) : BOOLEAN;
其中inList是儲存了輸出序列的陣列,即inList[] = [1..n];outList是儲存了該輸出序列的陣列,len為序列長度,輸出序列正確返回true,否則返回false。
例如,當len= 4時,若outList [4] = [1,2,3,4],則返回true;若outList [4] = [1,4,2,3],則返回false。
子淵拿到題目後,大腦立刻高速運轉起來:
要模擬所有列車的入棧和出棧過程,也就是要跟蹤整個過程,直到出現錯誤或者列車全部出棧。在排程過程中,我用i表示入棧序列中最前方列車的編號(如果列車已經全部入棧,則i=len);用j表示當前出棧序列中出棧列車的編號(如果j = len,則表示所有列車都能出棧,出棧序列正確);用棧s儲存當前停在站內的列車。初始時,i = j = 0,s為空。
在排程過程中:
如果j = len,則表示所有列車都能出棧,出棧序列正確;否則
如果inList[i] = outList[j],則表示列車進棧後馬上出棧;否則
如果s非空且GetTop(s, top) = outList[j],那麼讓outList[j]出棧;否則
如果i<len,即還有列車未入棧,則讓inList[i]入棧;否則
說明列車已全部進棧,但不滿足後進先出原則,出錯。
根據上述分析過程,子淵給出了程式碼:
{程式碼8:}
{模擬各元素的入棧和出棧過程,判斷一個輸出序列是否正確}
PROGRAM TrainList(INPUT, OUTPUT);
CONST
MAXCAPACITY = 255; {棧的最大容量}
BOTTOM = 0;{棧底標誌}
TYPE
ElemType = integer;{棧內元素資料型別}
Stack= array [1..MAXCAPACITY] of ElemType; {用陣列表示的棧}
List= array [1..MAXCAPACITY] of ElemType; {用陣列表示的出入棧序列}
VAR
inList, outList : List; {定義出入棧序列}
s: Stack;{定義s為棧}
top: integer;{棧頂標誌}
i, n : integer;
。。。。。。{此處為棧的基本操作函式,不再重複列出}
FUNCTION TrueList(inList, outList : List; len : integer) : BOOLEAN;
var
i, j : integer;
begin
i := 1;
j := 1;
while j < len do {如果j = len,則表示出棧序列正確}
begin
if inList[i] = outList[j] then {列車進棧後馬上出棧}
begin
inc(i);
inc(j);
end {if}
else if (not StackEmpty(s, top)) and (GetTop(s, top) = outList[j]) then
begin
Pop(s, top); {出棧}
inc(j);
end {else if}
else if i <= len then {還有列車未進棧,令其進棧}
begin
Push(s, top, inList[i]); {入棧}
inc(i);
end {else if}
else{列車已全部進棧,但不滿足後進先出原則,出錯}
begin
TrueList := false;
exit;
end; {else}
end; {while}
TrueList := true;
end; {TrueList}
BEGIN {MAIN}
top := 0; {棧頂初始化}
writeln('len of inList:');
readln(n);
for i:=1 to n do{入棧序列}
inList[i] := i;
writeln('Input outList:');
for i:=1 to n do{出棧序列}
read(outList[i]);
if TrueList(inList, outList, n) then
writeln('true!')
else
writeln('false!');
END.
隨著最後一個字元地輸入,火車進站的汽笛響了,子淵和爸爸媽媽趕緊收拾好行李,準備上車。漫漫的旅程開始了,正如我們的小子淵,踏上了演算法學習的征程,前面的道路雖然曲折而遙遠,但是前進的旅途中,我們總能欣賞到一道又一道美麗的風景。