1. 程式人生 > 其它 >火車進棧

火車進棧

火車進棧

這裡有$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/