火车进栈

火车进栈

这里有$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/

posted @ 2021-12-04 20:45  onlyblues  阅读(303)  评论(0编辑  收藏  举报
Web Analytics