栈的应用

关于栈的应用拓展

栈是常用的一种数据结构,有n令元素在栈顶端一侧等待进栈,栈顶端另一侧是出栈序列。你已经知道栈的操作有两·种:push和pop,前者是将一个元素进栈,后者是将栈顶元素弹出。现在要使用这两种操作,由一个操作序列可以得到一系列的输出序列。请你编程求出对于给定的n,计算并输出由操作数序列1,2,…,n,经过一系列操作可能得到的输出序列总数。

输入

一个整数n(1<=n<=15(50%), 1<=n<=1000(100%))

输出

一个整数,即可能输出序列的总数目。

此题最原始的方法是利用回溯,对栈操作的模拟,边界是
       1.所有的元素全部出栈
       2.所有的元素经过操作


#include <stdio.h>
/*
    一个操作数序列,从1,2,一直到n,栈A的深度大于n。现在可以进行两种操作:
    1.将一个数,从操作数序列的头端移动到栈的头端(对应数据结构栈的push操作)
    2.将一个数,从栈的头端移动到输出序列的尾端(对应数据结构的pop操作)
    现在对于任意一个N,输入端的数据一定是1,2,3...N,求出可能出现的输出端数据序列的种数。
    解题思路:
    就是利用递归回溯,每一个元素只可能进栈出栈一次,所以当所有元素都进过栈后统计所有情况的次数
*/
int sum;
/*
    递归模拟入栈出栈的过程,因为问题是求解出栈的序列可能情况种类。
    因此在进行递归操作时要:先出栈,再入栈
*/
void dfs(int top,int head,int n)    //top 为栈顶位置,head为操作数序列(队列),经过一系列操作后队首位置
{
    if(head==n+1)    //所有元素都已经进栈操作过一次,该次入栈(出栈)操作结束
    {
        sum++;
        return;
    }
    if(top>0)
    {
        dfs(top-1,head,n);   /*未出过栈 ,则出栈*/
    }
    if(head<n+1)
    {
        dfs(top+1,head+1,n); /*未入过栈,则入栈*/
    }
}
int main()         //注意使用递归的方法去模拟出栈,入栈的操作,当数据稍大的时候,将会出现超时,或爆栈
{
    int n;
    scanf("%d",&n);
    sum=0;
    dfs(0,1,n);    //开始时,栈为空,且操作数队列队首head指向序列第一个元素的位置
    printf("%d\n",sum);
    return 0;
}
回溯法理解简单,但是效率奇差,n在比较小的情况下能够胜任,但是n一旦达到比较大的数时,必定超时!那有没有效率更高的算法呢?

回溯法效率差的原因是记录了很多的重复运算,其实此题还可以用动归方法解决,f[i,j],i表示入栈的个数,j表示出栈的个数,那f[i,j]就表示入栈i个数中出j个数的,但是此题要注意的是出栈数不能大于入栈数,那动归方程该如何推导,再次谢谢一位某位具有探索精神的大神为我们做了细致的研究现在我把它的论文粘贴如下:


1      引 言

在实际应用和数据结构课程的教学中,栈作为一种基本结构非常重要[1][3][4][6]。已知给定序列,求出栈序列的数目、求所有出栈的序列、以及判断某个序列是否为合法的出栈序列[5][7],这类问题经常出现。在[3]中,对出栈序列的计数问题给出了介绍性的说明,由于结果的证明需要用到生成函数,[3]也只是直接给出了结论。本文提出使用“两点之间路径计数”的方法解决出栈序列的计数问题,在此基础上可以求所有出栈的序列,以及判断某个序列是否为合法的出栈序列。

2      问题分析

           两点之间路径计数的问题

问题:假设A、B两点之间所有道路如图1中的方形网格线(5×5),规定从A到B只能够向右或向上移动,求A点到B点有几条路。

图1 两点之间路径计数问题的路径图

分析:因为规定从A到B只能够向右或向上移动,因此任意一点只能从该点的左邻点或下邻点到达,例如,任意一点C点只能从D点或E点到达。因此,从A点到C点的路只能由这两部分组成:①从A点到D点,再从D点到C点;②从A点到E点,再从E点到C点。

结论1:A点到任意一点C的路径数目=从A点到D点(C的左邻点)的路径数目+从A点到E点(C的下邻点)的路径数目

其中,由于A点正上方的点没有左邻点,而且问题中已规定从A到B只能够向右或向上运动,所以A点到A点正上方点的路径数目为1。同理,A点到A点正右方点的路径数目为1。

根据结论1,将A点到任意一点的路径数目求出,如图1中网格线交点处的数字所示。

           栈的操作与两点之间路径计数问题的操作的比较

栈的操作有两种:入栈、出栈。其中需要注意的问题有三个:①所有节点入栈之后只能出栈;②栈空时只能入栈;③其它情况下入栈、出栈任意执行。

两点之间路径计数问题的操作有两种:向右移动、向上移动。其中需要注意的问题有三个:①移到最右边后只能向上移;②移到最上边只能向右移;③其它情况下上移、右移任意执行。

由以上分析可见,栈的操作与两点之间路径计数问题的操作有很大的相似性,不妨将入栈和右移相关联,出栈和上移相关联。但是,这样关联之后,由于两个问题并不等价(例如,图1中的D点,按照栈的操作是不可到达的),所以需要对图1中的所有点进一步分析。

           对操作关联后图1中点的分析

首先,在图1中添加A点到B点的对角虚线。这条虚线将所有的点分成三类:①虚线上的点;②虚线左上方的点;③虚线右下方的点。

其次,按照两点之间路径计数问题规定的操作容易得出:①从A点移到虚线上每一个点时,所执行的右移操作次数和上移操作次数相等;②从A点移到虚线左上方每一个点时,所执行的右移操作次数小于上移操作次数;③从A点移到虚线右下方每一个点时,所执行的右移操作次数大于上移操作次数。

再次,由于栈操作过程中的任意时刻必须有:入栈操作次数≥出栈操作次数(取等号时栈空)。

很明显,图1中虚线左上方的点按照栈的操作是不可到达的,虚线上的点恰好是栈空时的状态,虚线右下方的点按照栈的操作都可以到达,所以考虑修改图1中的路径图。

           改进后的路径图及规则

将图1中虚线左上方的点去掉后如图2所示(5×5方形网格线的下三角)。

图2 改造后的的路径图

规定:从A到B只能够向右或向上移动,右移为入栈操作,上移为出栈操作。

根据2.3的分析可得结论2:

①  从A点到A点正右方的点的路径数目 = 1;

②  从A点到每一行最左的点(考虑B点,不考虑A点)的路径数目 =从A点到该点的下邻点的路径数目;

③  从A点到其它任意一点C的路径数目=从A点到D点(C的左邻点)的路径数目+从A点到E点(C的下邻点)的路径数目;

④  按照栈的操作从A点开始到B点,图2中的所有点都是可到达的;

⑤  4个节点的入栈、出栈操作完全包含在图2中;

⑥  将⑤扩展得:N个节点的出栈、入栈操作完全包含在(N+1)×(N+1)方形网格线的下三角中。

在此仅对结论2第⑤点作一些说明:首先A点和对角线上的其它点表示栈空,只能入栈(右移);其次,移到最右的竖边时所有的元素都已经入栈,只能出栈(上移);再次,B点为最终状态,不能入栈也不能出栈;最后,其它的点可以任意入栈(右移)、出栈(上移)。所以4个节点的入栈、出栈操作完全包含在图2中。

从结论2中可以看到栈的操作与两点之间路径计数问题的操作在图2中是等价的。根据结论2,将A点到任意一点的路径数目求出,如图2中网格线交点处的数字所示,图2中虚线箭头表示了执行结论2第①点,图2中实线箭头表示了执行结论2第③点。

           结论2推广

将结论2加以推广得结论3:对于如图2的形式((N+1)×(N+1)方形网格线的下三角),规定从A到B只能够向右或向上移动,右移为入栈操作,上移为出栈操作,所求A点到B点的路径数目就是N个节点出栈序列的数目,并且从A点到B点的每一条路都代表一种出栈序列。

3      设计实现

求N个节点出栈序列数目的算法在具体实现时,采用由下向上逐行处理,每一行从左至右逐点处理的方法。此外,在 计算当前行的值时,只需要使用上一行的值;在计算各行中的每一个值时左邻点的值为数组中当前元素的前一个元素,下邻点为数组中当前元素,把数组中当前元素 的前一个元素加到当前元素上就求出了当前点的值,所以在具体实现时,只使用一个数组来保存当前行的值即可。由于最后一行只有一个数,也是该行的第一个数, 根据结论2第③点可知该数在倒数第二行中已经计算出来,所以该行不用计算,直接取上一行计算结果中的最后一个数即可。

4      结论

该方法简单方便,不需要记忆任何公式[3],特别适合没有组合数学基础的人员。另外,根据图2还可以设计算法将入栈、出栈的操作序列求出来,这样就可以得到所有的出栈序列。同时根据图2也可以判断某个序列是否为合法的出栈序列,可以解决[5][7]中车厢调度问题。

  为表感谢,特把该作者文章标红显示,通过该大神深入浅出的讲解,我相信只要不是比我还笨的人都能理解了,大神为了方便讲解所以把向右定义为入栈,向上定义为出栈,但为了方便书写代码,我把向下定义为出栈,向右定义为入栈,那么动归方程就推导如下f[i,j]:=f[i-1,j]+f[i,j-1], 代码如下:

问题分析:


        由题目可知,可能的动作只有进栈和出栈,且出栈次数<=入栈次数,因此可修改该问题的表现。假设N=3,建立一个(N+1)×(N+1)的网格,如下图所示(以线段交叉点为网格点):


        从网格左下角出发,规定向上走一步为一次入栈操作,向右走一步为一次出栈操作,由于出栈次数<=入栈次数,因此可行范围限定在对角线及其以上区域。设N(m,n)是经过n次入栈,m次出栈后可能的结果数量,由图可知,入栈n次,出栈m次,是由一次出栈或一次出栈形成的。因此N(m,n)=N(m-1,n)+ N(m,n-1),其中N(0,0)=1,即初始状态为1;左侧边界表示只能有入栈操作,因此N(0,n)=N(0,n-1)。图中×表示不可能出现的情况,因此也可以认为是0。这样N(3,3)便为元素为3的情况下所有可能的出栈序列的次数。

      该问题求解需要一个(N+1)×(N+1)的网格进行辅助运算,所以空间复杂度为O(n2),由于是从前往后的递推求解,因此可用两层循环嵌套的方式计算出网格内的所有点,因此时间复杂度为O(n2)。


#include <stdio.h>
#include <stdlib.h>
int D[13][13];//默认栈最大为13,如有需要请自行修改
void Stack(int M)
{
    int i,j;
    for (i=0;i<=M;i++)
    {
        for (j=0;j<=M;j++)
        {
            D[i][j]=0;
        }
    }
    for (i=0;i<=M;i++)
    {
        D[i][0]=1;
    }
    for (i=1;i<=M;i++)
    {
        for (j=1;j<=M;j++)
        {
            if (i>=j)
            {
                D[i][j]=D[i-1][j]+D[i][j-1];
            }
        }
    }
}
void main()
{
    int M;
    scanf("%d",&M);
    Stack(M);
    printf("%d", D[M][M]);
}
动归比回溯效率从阶乘的效率提高到n^2的效率,那此题还能继续优化吗,那是当然,那就是公式,据说此题是一个经典的卡特兰数。

栈是一种常见的数据结构,有许多关于栈的问题,其中之一就是统计元素可能的出栈序列。具体说,就是给定n个元素,依次通过一个栈,求可能的出栈序列的个数。
如果我们用直接模拟的方法,当n较大时会很费时间;另一种方法是利用组合数学求出栈序列个数,得到公式

下面我们来看一种图形化的方法证明这个等式,很容易理解的。
我们把对n个元素的n次进栈和n次出栈理解为在一个n * n的方格中向右走n次(代表进栈),向上走n次(代表出栈)。由于出栈次数不能大于进栈次数,我们可以得到这样一个方格:

每次沿着实线走,所以,只要求出从方格左下角到右上角路径的个数即可。我们把表格补全,考虑每一条不合法的路径,如

在这条路径上,必然有一个地方连续两次向上,即从图上蓝点处开始,而且这个点必然在如图所示的绿线上。我们以这个点为起点,把到左上角整条路经取反,也就是对称上去,得到一条新路径,但是超出了表格。我们知道,这条路径包括n + 1次向上和n – 1次向下,也就是在一个(n + 1) * (n - 1)的方格中。由此我们知道,一条不合法路径必然对应一个(n + 1) * (n - 1)方格中的路径。同样地,对于(n + 1) * (n - 1)方格中任意一条路径,以这条路径与绿线的第一个交点为起点到方格的右上方全部取反,即可得到一个在n * n方格中的不合法路径。

我们通过这样的方法确定了每条不合法路径与一个(n + 1) * (n - 1)方格中路径的一一对应关系,因此,方格中不合法路径总数为C(2n, n - 1),而所有路径总数为C(2n, n),两式相减即为原组合等式。
注意数据溢出问题。适当在做乘法的过程中做下除法

#include<iostream>
using namespace std;
long long int f(int n)
{
    if (n == 0 || n == 1)
    {
        return 1;
    }
    return n*f(n - 1);
}
long long int fsum(int start, int end)
{
    long long int sum = 1;
    for (int i = start; i >= end; i--)
    {
        sum *= i;
    }
    return sum;
}
int main()
{
    int n;
    long long int s;
    while (cin >> n)
    {
        long long s1, s2;
        s1 = fsum(n * 2, n + 2) / f(n - 1);
        s2 = fsum(n * 2, n + 1) / f(n);
        s = s2 - s1;
        cout << s << endl;
    }
    return 0;
}


 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值