出栈序列的可能性判定

问题说明

PAT1051:给定 stack 的容量,给定数据的入栈顺序:从 1 开始的正整数序列,在允许随机的出栈操作的情况下,要求判断某出栈序列是否可能。

比如,告知 stack 容量为 5,入栈序列的最大值为 7。有两个序列需要判断合理性:

  • 1 2 3 4 5 6 7: 这个序列是可能的,只需每次入栈时都做出栈操作。
  • 3 2 1 7 5 6 4: 这个序列是不可能的,其中前半部分3 2 1是合法的,先将1 2 3顺序入栈,然后三次执行出栈操作。而之后的7 5 6则是不可能的。

要完成判定过程,常规思路是直接使用的 stack 数据结构模拟出栈序列做操作,然后判定是否会触犯条件。但考虑到 PAT1051 中时间限制只有 10ms,虽然常规方法是线性的,似乎也无法保障(事实证明是错误的,用常规方法也能在 PAT 上 AC),我想到从序列本身的特性入手,找规律,于是有了一种效率更高的判定逻辑。

常规思路

直接使用出栈序列指导 stack 模拟操作。判定条件有两条:

  • 1.栈中数据量不超过栈的容量。
  • 2.出栈只能从栈顶取,不应该出现从固定的堆栈中取出其他数据的情况。

算法描述如下:

用游标记录当前已知压栈的最大数据 cur。如果新的读入数据 tmp(即出栈序列中的某数据)大于 cur,则将 cur 到 tmp 之间的数据顺序压入栈中,更新 cur 并执行检查 1;如果新的读入数据 tmp 小于 cur,则一定是直接出栈获得的,执行检查 2。

如果能顺利完成就是合理的,如果操作过程违背了一些规则,则判定为不合理。C++实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<stdio.h>
#include<stack>
using namespace::std;
int m, n, k, tmp, cur;
bool flag;
stack<int> s;
int main()
{
    scanf("%d %d %d", &m, &n, &k);
    while(k --) {
        flag = true;
        cur = 1;
        s.push(1);
        for (int i = 0; i != n; ++ i) {
            scanf("%d", &tmp);
            if (tmp > cur) {
                for (int j = cur + 1; j <= tmp; ++ j)
                    s.push(j);
                if (s.size() > m) flag = false;
                cur = tmp;
            }else {
                if (s.top() != tmp)
                    flag = false;
            }
            s.pop();
        }

        if (flag) printf("YES\n");
        else printf("NO\n");
    }
}

更高效的判定逻辑

实际上,在 PAT1051 的环境下,由于入栈序列数据由小到大排列非常特殊,要通过出栈序列判定可能性是存在简便思路的。

对比分析题中 Sample 给出的序列,结合上面提到的两条冲突条件入手分析:

  • 1.栈中数据量不超过栈的容量:

    只有在入栈时,才会需要考虑栈中数据是否超量。出栈序列中的每个数,都以为着在出栈操作之前,它刚入栈,那么当它入栈的时候能否判定是否超过栈容量呢?可以的,(当前的出栈数值 - 已经执行过的出栈操作数量)就是当前栈中元素的数量。

  • 2.出栈只能从栈顶取,不应该出现从固定的堆栈中取出其他数据的情况。

    根据观察分析发现,当某数据 m 出栈之后,比 m 小的数据如果在 m 之后出栈的,它们所组成的序列本身需要保持从大到小的顺序排列。距离如3 2 1 7 5 6 4这个序列,在7之后有5 6 4这个子序列,它们都大于7,但却没有保持一个递减的顺序,不合法。

C++实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<stdio.h>
int m, n, k;
int max, min, tmp;
bool flag;
int main()
{
    scanf("%d %d %d", &m, &n, &k);
    while(k --) {
        flag = true;
        max = 0;
        min = 1001;

        for (int i = 0; i != n; ++ i) {
            scanf("%d", &tmp);
            if (tmp > max) {
                if (tmp - i > m) flag = false;
                else max = min= tmp;
            } else {
                if ( tmp > min) flag = false;
                else min = tmp;
            }
        }

        if (flag) printf("YES\n");
        else printf("NO\n");
    }
}

总结

在我的理解之中,经典的算法、数据结构是在面对编程问题的解决过程中所抽象出的通用模型。而生活是多变的,并不像考试卷一样简单的套用数学题所能解决,很多情况下,编程问题也是如此。那么除了这些经典的方法外,认真分析条件,并进行针对性的优化甚至重新设计就非常重要了。这里仅仅是一个小实践。

转载于:https://www.cnblogs.com/biaobiaoqi/p/3288725.html

<think>嗯,我现在得解决这个问题,判断一个给定的出栈序列是否合法。题目是用C++来编写程序。那我应该怎么思考呢? 首先,我得回忆一下栈的特性。栈是后进先出的结构,所以出栈的顺序必须满足一定的条件。比如,如果一个元素出栈了,那在它之后入栈的元素不能在它之前出栈。或者说,出栈序列必须是在进栈过程中可能产生的顺序。 那问题就转化为,如何模拟这个过程,看看给定的出栈序列是否可能。我记得通常的做法是使用一个辅助栈来模拟入栈和出栈的过程。具体来说,可以按照原序列的顺序依次将元素压入辅助栈,同时检查当前栈顶元素是否等于目标出栈序列的当前元素。如果相等,就弹出栈顶,并移动到目标序列的下一个元素。最终,如果辅助栈为空,说明这个出栈序列是合法的。 那具体步骤应该是这样的: 1. 初始化一个空栈。 2. 遍历目标出栈序列的每个字符。 3. 对于当前目标字符,先检查栈顶是否等于它。如果是,弹出栈顶,继续下一个目标字符。 4. 如果不是,就将原始序列中的元素依次压入栈,直到栈顶等于当前目标字符或者原始序列已经全部压入。 5. 如果原始序列已经全部压入,但栈顶仍然不等于目标字符,说明这个序列不合法,返回false。 6. 如果所有目标字符都处理完毕,并且栈为空,或者剩下的栈中元素按顺序弹出也能匹配,则返回true。 不过可能有些细节需要注意。比如,原始序列的顺序是固定的,入栈必须按顺序压入。所以模拟的时候,必须按照原始序列的顺序一个一个压入栈,不能跳跃或者改变顺序。 比如,原始序列是a b c d e,那么入栈顺序必须是先a,然后b,然后c等等。当模拟时,我们只能按这个顺序压入栈,然后根据目标序列中的元素来决定何时弹出。 举个例子,样例输入是目标序列a b c d e。这种情况下,每个元素都是入栈后立即弹出,所以结果是true。而另一个例子,比如原始序列是a b c,目标序列是c b a,这也是合法的,因为a入栈,b入栈,c入栈,然后弹出c,b,a。但如果目标序列是c a b,则不可能,因为当弹出c之后,栈顶是b,此时要弹出a必须先弹出b,所以不可能。 所以,具体的算法实现步骤应该是这样的: - 原始序列保存在一个数组或者队列中,按顺序处理。 - 目标序列保存在另一个数组或队列中,逐个检查。 - 使用一个栈来模拟入栈和出栈操作。 具体步骤: 初始化栈为空,指针i指向原始序列的第一个元素,指针j指向目标序列的第一个元素。 循环: 当栈不为空且栈顶等于目标序列的当前元素(j指向的),则弹出栈顶,j++。 否则,如果还有原始元素未处理(i < N),则将当前原始元素压入栈,i++。 否则,无法匹配,返回false。 重复这个过程,直到j到达目标序列末尾,且栈为空,则返回true。否则,返回false。 那这样是否正确呢? 比如,假设原始序列是a b c d e,目标序列是a b c d e。那么每次压入一个元素,立即弹出。i和j同时移动,直到结束,返回true。 另一个例子,假设原始序列是a b c d,目标序列是d c b a。则入栈顺序是a入栈,此时栈顶不是d,继续压入b,压入c,压入d。此时栈顶是d,等于目标序列第一个元素d,弹出,j移动到c。此时栈顶是c,弹出,依此类推,直到栈空,返回true。 再比如,目标序列是c a b的情况。假设原始序列是a b c。当处理到c时,需要先压入a、b、c。弹出c,此时栈顶是b。然后目标序列的第二个元素是a。此时栈顶是b,无法匹配,且原始序列已经没有元素可压入,所以返回false。 所以这个算法应该是可行的。 那现在要如何用C++来实现这个逻辑呢? 首先,输入的处理。输入的原始序列和目标序列都是按顺序输入的,每个元素之间用空格分隔。比如第三行的输入是类似“a b c d e”的字符串,需要将其分割成字符数组。 那么,在C++中,如何处理这样的输入? 可能需要读取每行的输入,然后按空格分割。例如,第一行是N,第二行是原始序列的元素,第三行是目标序列的元素。 可以用getline来读取每一行,然后通过字符串流来分割每个字符。 比如,对于第二行的输入,假设输入的是"a b c d e",可以通过istringstream来逐个读取每个字符,存入一个vector<char>中。同样的,第三行的输入也做同样处理。 需要注意的是,输入的每个元素之间可能有多个空格,或者开头或结尾有空格?不过根据题目描述,输入是“按顺序输入该序列的所有元素,输入顺序即为进栈的顺序”,所以应该不会有这样的问题。但处理的时候,最好还是分割所有非空格的部分,不过题目中的元素是单个英文字母,所以每个元素之间用一个空格分隔。 因此,读取输入的方法可以是: 读取第二行,分割为字符数组original。 读取第三行,分割为字符数组target。 然后检查这两个数组的长度是否等于N,如果不等于的话,直接返回false?比如,如果输入的第三行元素数目不等于N的话,那肯定不合法。所以,在程序中应该首先检查目标序列的长度是否等于N,否则直接返回false。 所以,程序的步骤: 1. 读取N。 2. 读取第二行,分割成original数组。 3. 读取第三行,分割成target数组。 4. 检查original和target的长度是否都是N。如果不是,返回false。 5. 然后,模拟栈的过程。 接下来,如何模拟? 使用一个栈,比如stack<char> s。 然后,i是原始数组的当前索引,初始为0。j是目标数组的当前索引,初始为0。 循环的条件是:当j还没有处理完所有目标元素时。 在每次循环中: - 如果栈不为空,并且栈顶元素等于target[j],那么弹出栈顶,j++。 - 否则,如果i < N,将original[i]压入栈,i++。 - 否则,无法继续,返回false。 循环结束后,如果j已经处理完所有元素,并且栈为空,或者栈中剩下的元素可以按顺序弹出匹配剩下的目标元素?或者是否在循环中已经处理完了? 比如,假设所有原始元素已经压入栈,剩下的目标元素必须按栈的顺序弹出。所以,在循环中,只要栈顶元素等于当前target[j],就弹出。否则的话,无法继续。 所以,当原始元素已经全部压入栈之后,剩下的处理只能是不断弹出栈顶元素,看是否能匹配剩下的目标元素。如果在此时,栈顶元素不等于target[j],那么直接返回false。 所以,这个循环的条件可能需要持续处理,直到j到达末尾或者无法处理。 或者,可以这样处理: 在循环中,每次处理一个步骤: 当j < N时: 如果栈非空且栈顶等于target[j],则弹出,j++。 否则,如果i < N,压入original[i++]。 否则,返回false。 这样,循环直到j到达N。此时,如果栈为空,则返回true,否则返回false?或者是否还需要处理? 比如,假设循环结束的条件是当i和j都已经处理完。例如,当i已经压入所有元素,并且栈顶无法匹配target[j],就会返回false。否则,当j到达N时,说明所有目标元素都被处理了,此时应该返回true。 比如,假设循环中处理完所有的j,那么此时应该返回true。 比如,考虑样例输入:当所有的元素都压入并弹出,此时i和j都等于N,循环结束,返回true。 另一个情况,假设原始序列是a b c,目标序列是a c b。那么: 初始时,栈空,i=0,j=0。 进入循环: 栈空,所以压入original[0]即a。i=1。 此时栈顶是a,target[0]相等。弹出,j=1。栈空。 现在,j=1,target[j]是c。栈空,所以压入original[1]即b。i=2。栈顶是b,不等于c。继续压入original[2],即c。i=3。此时栈顶是c,等于target[1]。弹出,j=2。此时栈顶是b,等于target[2]。弹出,j=3,循环结束。返回true。 所以这个逻辑是正确的。 那这个模拟的过程是正确的。所以,可以用这个逻辑来实现。 那现在具体在C++中的代码结构: 首先,读取输入: 读取N。注意N的范围是3<N<26。所以输入可能是一个整数,比如5。读取第一行,转换成int。 然后读取第二行,按空格分割得到original数组。可以使用getline(cin, line),然后用istringstream分解。 同样的第三行处理得到target数组。 然后,检查original和target的大小是否为N。如果不同,直接输出false。 然后模拟栈的过程: 初始化i=0,j=0。stack<char> s。 循环: while (j < N) { if (!s.empty() && s.top() == target[j]) { s.pop(); j++; } else { if (i < N) { s.push(original[i]); i++; } else { // 无法匹配,返回false return false; } } } 循环结束后,此时j应该等于N,且栈可能为空或不为空? 比如,当栈中的元素在循环中被弹出完毕,此时j到达N,返回true。否则,如果在循环中,j还没到达N就已经无法处理,就会返回false。 例如,当所有元素都处理完毕,此时栈可能还有元素吗? 比如,假设原始序列是a b c d,目标序列是a b c。此时N=4,但目标序列的长度是3,那么在步骤4检查的时候就会返回false。所以这种情况不会进入模拟步骤。 所以,在模拟过程中,当j循环到等于N时,说明所有目标元素都被处理,此时返回true。 例如,假设原始序列是a b c d e,目标序列是a b c d e。那么每次压入一个元素,弹出,i和j同步增加。最后,当j=5(N=5),循环结束,返回true。 或者,当栈中还有元素,但j已经处理完所有目标元素?比如,假设N=3,原序列是a b c,目标序列是a。这时候原序列和目标序列长度不同,所以步骤4直接返回false。所以在模拟时,目标序列的长度一定是等于N的。 所以,模拟结束后,返回true即可。 综上,整个算法的正确性应该没问题。那现在代码的编写需要注意输入的处理。 那现在处理输入的步骤: 比如,对于第二行的输入,用户输入的是“a b c d e”,每个字符之间有一个空格。用getline读取之后,用istringstream分割。 例如: string line; getline(cin, line); // 读取第二行 istringstream iss(line); vector<char> original; char ch; while (iss >> ch) { original.push_back(ch); } 同样处理第三行的目标序列。 然后检查original.size()和target.size()是否等于N。如果不等于,输出false。 否则进行模拟。 那现在编写C++代码的大纲: #include <iostream> #include <stack> #include <vector> #include <sstream> using namespace std; int main() { int N; cin >> N; cin.ignore(); // 忽略第一行后面的换行符,以便后续getline正确读取 vector<char> original; string line; getline(cin, line); istringstream iss_original(line); char ch; while (iss_original >> ch) { original.push_back(ch); } vector<char> target; getline(cin, line); istringstream iss_target(line); while (iss_target >> ch) { target.push_back(ch); } if (original.size() != N || target.size() != N) { cout << "false" << endl; return 0; } stack<char> s; int i = 0, j = 0; while (j < N) { if (!s.empty() && s.top() == target[j]) { s.pop(); j++; } else { if (i < N) { s.push(original[i]); i++; } else { cout << "false" << endl; return 0; } } } cout << "true" << endl; return 0; } 这样是否正确? 比如,样例输入: 5 a b c d e a b c d e 处理后的original和target的大小都是5,符合条件。然后模拟时,每次压入一个元素,栈顶等于target[j],弹出。i和j同步增加。循环结束后输出true。 另一个测试用例,假设N=5,原序列a b c d e,目标序列是d e c b a。这可能吗? 假设原序列是a b c d e,目标序列是d e c b a。模拟过程: 初始i=0,j=0. 栈为空,压入a。i=1。栈顶是a,不是d。压入b。i=2。压入c。i=3。压入d。i=4。栈顶是d,等于target[0]。弹出,j=1。此时栈顶是c。目标[j]是e。不是,所以压入e(i=4,此时i=5?原序列是a b c d e,i初始为0。压入a(i=0)、压入b(i=1)、压入c(i=2)、压入d(i=3),此时i=4。此时栈顶是d,弹出后,i还是4。然后,现在j=1,目标元素是e。栈顶是c。不等于e,所以需要压入下一个元素。i=4,压入e。i=5,此时i等于N(5)。现在栈顶是e,等于target[j=1]。弹出,j=2。此时栈顶是c。目标[j=2]是c。弹出,j=3。栈顶是b。目标[j=3]是b。弹出,j=4。栈顶是a。目标[j=4]是a。弹出,j=5。循环结束。输出true。所以这个目标序列是合法的。所以程序会输出true。 另一个情况,假设原序列是a b c,目标序列是c a b。则原序列长度和目标序列长度都是3。模拟过程: i=0,j=0. 栈空,压入a。i=1。栈顶是a,不是c。压入b。i=2。栈顶是b,不是c。压入c。i=3。此时栈顶是c,等于target[j=0]。弹出,j=1。此时栈顶是b。目标[j=1]是a。不等于。i已经等于3(N=3),无法继续压入。返回false。输出false。正确。 所以这段代码应该能正确处理这些情况。 但有一个问题,原题目中的输入N的条件是3 < N < 26。比如N的范围是4到25。所以在代码中是否需要处理输入的N是否符合条件?题目可能给出的测试用例中的N是合法的,所以可能不需要,但实际编程题可能要考虑输入合法性。但题目中的输入形式可能保证输入的N在范围内。但根据题目描述,用户输入的N可能不合法?比如用户输入N=3,这时候是否需要处理? 题目中的问题描述中,输入形式的第一行给出N的值,要求3<N<26。所以在程序中是否需要判断输入的N是否符合条件?例如,如果输入N=4,是否合法?是的,因为3<4<26。但例如,输入N=3的话,则不符合,应该如何处理? 但题目中样例输入给出的是5,输出true。所以可能需要程序不处理N是否合法,假设输入总是合法的。这可能吗?或者是否需要处理? 题目中是否要求程序在输入N不符合条件时如何处理?问题描述中没有提到,所以可能题目中的测试用例保证输入的N是合法的。所以在代码中可以不需要处理N的输入是否合法。 综上,代码应该正确。</think>要判断一个给定的序列是否为合法的出栈序列,可以通过模拟栈的入栈和出栈过程来实现。以下是详细的解决步骤: ### 方法思路 1. **输入处理**:读取元素个数N、原始序列和目标序列。 2. **合法性检查**:确保原始序列和目标序列的长度均为N。 3. **模拟栈操作**: - 使用一个辅助栈来模拟入栈过程。 - 依次将原始序列的元素压入栈,同时检查栈顶元素是否目标序列的当前元素匹配。 - 若匹配则弹出栈顶元素并处理目标序列的下一个元素;若不匹配则继续压入原始序列的元素。 - 若所有元素已压入栈但仍无法匹配目标序列,则判定为非法序列。 ### 解决代码 ```cpp #include <iostream> #include <stack> #include <vector> #include <sstream> using namespace std; int main() { int N; cin >> N; cin.ignore(); // 忽略换行符 // 读取原始序列 string line; getline(cin, line); istringstream origStream(line); vector<char> original; char ch; while (origStream >> ch) { original.push_back(ch); } // 读取目标序列 getline(cin, line); istringstream targetStream(line); vector<char> target; while (targetStream >> ch) { target.push_back(ch); } // 检查长度是否匹配 if (original.size() != N || target.size() != N) { cout << "false" << endl; return 0; } stack<char> stk; int i = 0, j = 0; // i遍历原始序列,j遍历目标序列 while (j < N) { // 栈顶元素匹配目标序列当前元素 if (!stk.empty() && stk.top() == target[j]) { stk.pop(); j++; } else { // 还有元素可入栈 if (i < N) { stk.push(original[i]); i++; } else { // 无法继续匹配 cout << "false" << endl; return 0; } } } cout << "true" << endl; return 0; } ``` ### 代码解释 1. **输入处理**:使用`getline`和`istringstream`读取并分割输入序列。 2. **长度检查**:确保原始序列和目标序列的长度均为N,否则直接返回`false`。 3. **栈模拟**: - 遍历目标序列,若当前栈顶元素目标元素匹配,则弹出栈顶并处理下一个目标元素。 - 否则,将原始序列的元素依次压入栈,直到栈顶匹配或所有元素已压入。 - 若所有元素已压入仍无法匹配,则判定为非法序列。 此方法通过模拟栈的实际操作过程,确保在时间复杂度为$O(N)$的情况下高效判断出栈序列的合法性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值