栈是一种线性的、只能在一端操作的、后进先出的数据结构。
Rails(poj1363)
若1,2,3,4,...,n依次进栈(但不一定是依次连续进栈),然后再出栈,判断某个出栈序列的合法性。
思路:人工模拟进栈、出栈的过程。例如,1,2,3,4,5依次进栈,现有出栈序列3,4,2,1,5,问是否合法?
第一个出栈的是3,栈目前空,所以1,2,3进栈;
栈目前非空,为1,2,3,栈顶是3;
3出栈;
第二个出栈的是4,栈目前非空,为1,2,栈顶是2;
4进栈,栈目前非空,为1,2,4;
4出栈;
第三个出栈的是2,栈目前非空,为1,2,栈顶是2;
2出栈;
第四个出栈的是1;1出栈;
第五个出栈的是5,栈目前空,但还有未进栈的,所以5进栈;
栈目前非空,为5;
5出栈;
栈空。
元素遍历完毕(意味着
都进栈了),出栈序列遍历完毕(意味着
都出栈了),因此序列合法。
代码:
#include <iostream>
using namespace std;
#include <stack>
bool if_perm_ok(int *p, int size)
{
stack<int> tmp; //申请一个栈,已初始化为空栈
int i=0, j=1; //i用来标记序列元素p[i],j用来表示待进栈的元素1,2,3,4,5
while(j <= size+1) //待进栈元素还没有用完
{
if(tmp.empty()) //若栈为空
{
if(j == size + 1) break; //一种情况是所有元素入栈后均出栈,为正常清空,此时j == size + 1,跳出while循环
else tmp.push(j++); //另一种情况是过程中的某一点栈暂时为空,此时需要push当前元素
}
else //若栈非空,判断栈顶元素与序列中的当前指向元素
{
if(tmp.top() == p[i]) {tmp.pop(); ++i;} //栈顶元素与出栈序列中的当前元素相同,说明该它出栈了;并指向序列中的下一个元素
else tmp.push(j++); //若不同,说明该使下一个元素进栈;如果序列是非法的,这一步会导致j>size,从而跳出while循环
}
}
if (i+1 == j) return true;
else return false;
}
这一算法机械地模拟了判断过程,
时间复杂度为O(n)。
另有一
规律,合法的出栈序列,
最大元素之后的元素必须单调递减排列。可以作为选择题的判断依据。
想象(脑补进出栈的动画过程):在任意时刻,栈中的元素不管是否连续,从栈底到栈顶,总是递增的。
当栈再无元素可入时,必定是最大的元素在栈顶,此后只能出栈,因此,此后的序列必定是递减的。注意,这是序列合法的
必要条件而非充分条件,举个反例:3,1,6,5,4,2。
Catalan数
出栈序列总数是问题规模n的函数f(n),则:
f(1) = 1;
f(2) = 2;
f(3) = 5; 123, 132, 213, 231, 321
f(4) = ?
分解成子问题递归求解:
(1)()()()---f(0)*f(3)
()(1)()()---f(1)*f(2),且第一个括号里只能是2
()()(1)()---f(2)*f(1),且前两个括号里只能是2,3
。因为1在第三个出栈,之前2必然已经出过栈,3必然已经出过栈,所以前两空必然是2和3。
()()()(1)---f(3)*f(0)
所以,f(4) = f(0)*f(3) + f(1)*f(2) + f(2)*f(1) + f(3)*f(0)。
f(n) = (sigma(i=0)to(n-1))f(i)f(n-1-i),这是一个O(n^2)的算法,因为要递归计算f(n-1), f(n-2)...f(0),每计算一个f(i)都需要i步。
有O(1)的算法称为Catalan数,f(n) = C(2n, n) - C(2n, n-1) = C(2n, n) / (n+1)。可用“折线法”形象化地证明。