火車進棧
火車進棧
這裡有$n$列火車將要進站再出站,但是,每列火車只有$1$節,那就是車頭。
這$n$列火車按$1$到$n$的順序從東方左轉進站,這個車站是南北方向的,它雖然無限長,只可惜是一個死衚衕,而且站臺只有一條股道,火車只能倒著從西方出去,而且每列火車必須進站,先進後出。
也就是說這個火車站其實就相當於一個棧,每次可以讓右側頭火車進棧,或者讓棧頂火車出站。
車站示意如圖:
出站<—— <——進站
|車|
|站|
|__|
現在請你按《字典序》輸出前$20$種可能的出棧方案。
輸入格式
輸入一個整數$n$,代表火車數量。
輸出格式
按照《字典序》輸出前$20$種答案,每行一種,不要空格。
資料範圍
$1 \leq n \leq 20$
輸入樣例:
3
輸出樣例:
123 132 213 231 321
解題思路
我的思路
首先,看到資料範圍大小就知道應該要用dfs了。
這道題雖然打上了簡單的標籤,但當時想了很久,最後還是用直覺來AC的。
說一下我當時的思路吧,一開始想錯了,但最後的思路與正解幾乎一樣。
我一開始是怎麼想的呢。因為搜尋嘛,所以肯定先想搜尋的順序或狀態。所以我一開始想的是,分兩種狀態,一種是一個數字壓棧裡,另一種是數字出棧,所以搜尋的順序是先把數字壓棧裡,然後繼續從下一個數字開始搜。當回溯到這個數字時,再把數字從棧頂彈出,然後繼續搜。同時因為我是按數字從小到大的方式搜的,所以保證數字的的出入棧順序是合法的(因為只有小的數字入棧後,後面的數字才可以入棧)。
按照這個思路,然後我就寫出了這樣的程式碼。
#include <cstdio> #include <vector> #include <queue> #include <algorithm> using namespace std; const int N = 30; int q[N], hh, tt = -1; int stk[N], tp; priority_queue<vector<int>, vector<vector<int>>, greater<vector<int>>> pq;錯誤思路的程式碼void dfs(int cnt, int n) { // 遞迴邊界為搜尋到最後一個數字 if (cnt == n) { vector<int> ret; // 由於出棧的元素壓到佇列,所以先把佇列的元素壓入陣列中 for (int i = hh; i <= tt; i++) { ret.push_back(q[i]); } // 同時由於最後一個數字要先壓入棧,然後又最先從棧彈出,所以乾脆直接把最後一個數字壓入陣列中 ret.push_back(n); // 最後把棧中的元素壓入陣列中 for (int i = tp; i; i--) { ret.push_back(stk[i]); } // 把出棧的結果壓入優先佇列,到時候直接從優先佇列中輸出前20個結果就可以了 pq.push(ret); return; } stk[++tp] = cnt; // 先把數字壓入棧,然後往下一個數字搜 dfs(cnt + 1, n); q[++tt] = stk[tp--];// 恢復現場,同時把數字從棧頂壓入佇列 dfs(cnt + 1, n); // 繼續從下一個數字搜 tt--; } int main() { int n; scanf("%d", &n); dfs(1, n); for (int i = 0; !pq.empty() && i < 20; i++) { vector<int> tmp = pq.top(); pq.pop(); for (auto &it : tmp) { printf("%d", it); } printf("\n"); } return 0; }
當我們輸入3時,會發現輸出結果如下:
123 132 231 321
少了“213”這種情況,這就很奇怪了,說明這種搜尋方式是有問題的,我們來看看問題出現在哪裡。
要得到“213”,首先是1入棧,然後2入棧,接著把棧中的所有元素彈出,得到“21”,然後3再入棧出棧,就得到了“213”。
我們會發現,在遞迴到數字2時,1也跟著出棧了。順序是2先入棧,然後2出棧,然後1出棧。要知道,按照我們上面的思路或者程式碼邏輯,應該是1出棧後才會有2入棧。
我們應該解決的是,當回溯到後面比較大的數字時,前面比較小的數字如果還在棧中,要將其彈出來。
發現問題後,我當時的直覺是,考慮一種情況,棧中有元素未彈出,我們先把棧頂的一個元素彈出,再把數字壓入棧。回溯到這個數字時,先把這個數字從棧彈出,然後再把棧頂元素彈出,再把這個數字壓到棧。只要還能回溯到這個數字,就一種重複這個操作,直到棧為空時,再把數字壓入,繼續搜尋。
但事實時,我當時的程式碼實現是反過來的,是先把所有的元素從棧中彈出,然後再逐個把這些元素從佇列中壓回棧。操作和上面說的一樣,只不過我是反過來做的。
我也不知道為什麼在程式碼實現上是反過來做的,然後很幸運的是,正因為我是反過來做的,就陰差陽錯地實現了題目要求地按字典序輸出(後面我會解釋為什麼這樣子做可以保證是按字典序輸出)。所以才說這題我是憑著直覺AC的。
AC程式碼如下:
1 #include <cstdio> 2 #include <vector> 3 #include <algorithm> 4 using namespace std; 5 6 const int N = 30; 7 8 int q[N], hh, tt = -1; 9 int stk[N], tp; 10 int tot; 11 12 void dfs(int cnt, int n) { 13 if (tot >= 20) return; // 因為是按字典序輸出,所以搜尋完前20種結果後就可以結束了 14 15 // 其中每種結果遞迴邊界是已經搜尋到第n+1個數,說明已經得到前n個數的出棧順序了 16 if (cnt > n) { 17 tot++; 18 19 // 由於出棧的元素壓到佇列,所以先把佇列的元素壓入陣列中 20 vector<int> ret; 21 for (int i = hh; i <= tt; i++) { 22 ret.push_back(q[i]); 23 } 24 25 // 最後把棧中的元素壓入陣列中 26 for (int i = tp; i > 0; i--) { 27 ret.push_back(stk[i]); 28 } 29 30 for (auto &it : ret) { 31 printf("%d", it); 32 } 33 printf("\n"); 34 35 return; 36 } 37 38 int m = tp; // 記錄棧中元素個數 39 40 // 把棧中的元素先全部壓到佇列中 41 while (tp) { 42 q[++tt] = stk[tp--]; 43 } 44 45 // 要把m個元素壓回棧中。同時還要再考慮棧中沒有元素,把數字壓入棧這種情況,所以一個迴圈m+1次 46 for (int i = 0; i <= m; i++) { 47 stk[++tp] = cnt; // 把數字壓入棧,然後往下一個數字搜 48 dfs(cnt + 1, n); 49 tp--; // 把數字彈出 50 51 if (i != m) stk[++tp] = q[tt--]; // 然後再把佇列的元素壓入棧。執行到最後一次時,已經把原來的元素全部壓到棧中了 52 } 53 } 54 55 int main() { 56 int n; 57 scanf("%d", &n); 58 59 dfs(1, n); 60 61 return 0; 62 }
y總的思路
假設我們已經把前k個元素壓入過棧(這其中可能有些元素出棧了),例如下圖這種情況:
很自然會想到有兩種操作,第一種是把k壓入棧頂,第二種是把棧頂元素彈出。
遞迴的邊界是佇列中元素的個數是n。按照這種搜尋方式我們就可以把所以的出棧順序給枚舉出來。
接下來我們來考慮如何按字典序輸出。我們來考慮操作1和操作2這兩種執行順序的先後對出棧順序結果的影響。
首先我們會發現,序列中的元素都是大於棧中的元素,這是因為我們是按照編號從小到大的順序來進棧的。所以還沒有進棧的元素一定是比進棧的元素要大。
因此,如果我們一開始先執行操作2,把棧頂元素彈到佇列中去,那麼佇列中的下一個元素(也就是從棧頂彈出的元素),一定會比先執行操作1再執行操作2得到的佇列中下一個元素要小(原來棧頂的元素沒有彈出,反而一個更大的數被壓入棧頂,再按照出棧的順序,你可以想象一下,是更大的數彈出)。
這是因為,如果先執行操作1,也就是把一個更大的元素壓入棧,然後再去執行下一個操作(再去搜索下一層)。當回溯到這層時,這時就要把棧頂元素彈出壓入佇列,也就是這個更大的數壓入佇列。所以,可以發現,如果一開始就執行操作2,把棧頂那個相對更小的數壓入佇列,就可以得到一個更小的出棧順序了(字典序)。
好了,思路已經講完了,AC程式碼如下:
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 const int N = 30; 6 7 int tot; 8 int q[N], hh, tt = -1; 9 int stk[N], tp; 10 11 void dfs(int cnt, int n) { 12 if (tot >= 20) return; 13 14 // 遞迴的邊條件是佇列中的元素個數為n 15 if (tt - hh + 1 == n) { 16 tot++; 17 for (int i = hh; i <= tt; i++) { 18 printf("%d", q[i]); 19 } 20 printf("\n"); 21 22 return; 23 } 24 25 // 先執行操作2,也就是先把棧頂元素彈出 26 if (tp) { // 棧不為空才可以彈出 27 q[++tt] = stk[tp--]; 28 dfs(cnt, n); 29 stk[++tp] = q[tt--]; 30 } 31 32 // 再執行操作1,把序列中的元素壓入棧 33 if (cnt <= n) { // 要壓入的元素大小不可以大於n 34 stk[++tp] = cnt; 35 dfs(cnt + 1, n); 36 tp--; 37 } 38 } 39 40 int main() { 41 int n; 42 scanf("%d", &n); 43 44 dfs(1, n); 45 46 return 0; 47 }
參考資料
AcWing 129. 火車進棧:https://www.acwing.com/video/67/