P1044 栈(递归、递推、卡特兰、打表)

P1044 栈

题目背景
栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。

栈有两种最重要的操作,即pop(从栈顶弹出一个元素)和push(将一个元素进栈)。

栈的重要性不言自明,任何一门数据结构的课程都会介绍栈。宁宁同学在复习栈的基本概念时,想到了一个书上没有讲过的问题,而他自己无法给出答案,所以需要你的帮忙。

题目描述

在这里插入图片描述
宁宁考虑的是这样一个问题:一个操作数序列,1,2,…,n(图示为1到3的情况),栈A的深度大于n。

现在可以进行两种操作,

将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的push操作)

将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的pop操作)

使用这两种操作,由一个操作数序列就可以得到一系列的输出序列,下图所示为由123生成序列231的过程。
在这里插入图片描述

(原始状态如上图所示)

你的程序将对给定的n,计算并输出由操作数序列1,2,…,n经过操作可能得到的输出序列的总数。

输入格式
输入文件只含一个整数n(1≤n≤18)

输出格式
输出文件只有
1
1行,即可能输出序列的总数目

输入输出样例

输入 #1 复制
3
输出 #1 复制
5

题解如下

这一题看到许多大佬用到是 卡特兰 写的,的确这样写非常对但是应用面非常窄,所以我先用 其它常用的方法来写(这些方法都是从洛谷大佬那剽窃过来的😳 ),最后再用 catalan来写!

方法一

1 .递归/记忆化搜索
这一题如果我们直接递归的话会时间超限,于是我们就想到了记忆话dfs,这样可减少递归所用的时间

  • 思路如下:
  1. 既然选择了记忆化搜索的我们肯定要定义一个二维数组f[ i ][ j ] ,其中 i 表示在队列中等待进栈的元素个数,j 表示在栈里面的元素的个数,那么f[ i ][ j ] 则表示 在这种状态下的方案数。
  2. 一旦 f[ i ][ j ] 被计算过存在值,就直接返回。
  3. 剩下的就是考虑递归的实现了,首先我们明白,当 队列里面的元素为0的时候(即 i = 0),这时候 栈里面的元素只能出来,所以 这个状态下的方案数 为 1,这个自然也是递归的边界。
  4. 然后我们再考虑,递归不断进行时的状态变化,我们可以对栈是否为空进行讨论(因为栈是否为空,影响着对栈的操作pop、push),当 栈为空的时候,此时可以对栈进行的操作是push,把队列里面的一个元素拿出来放到栈里面 即为:f[ i ] [ j ] = f[ i - 1 ][ j + 1 ] ; 当 栈里面的元素元素 不为空的时候,我们可以对栈进行 push、pop操作,对于pop操作,就是把栈里面的元素弹出来,即为f[ i ][ j ] = f[ i ][ j - 1 ] ,所以此时能进行的操作为: f[ i ] [ j ] = f[ i - 1 ][ j + 1 ] 、 f[ i ][ j ] = f[ i ][ j - 1 ] 。此时便愉快的结束了。
    这一思路的答案会在下方与其他答案一起呈现,那么让我们来看看下一个思路吧!

2.递推/DP
这一题显然用递推更加安全,这里的递推与地柜有些不同,但大部分是相似的。

  • 思路如下:
  1. 我们仍然定义一个f[ i ][ j ], 这里的 i 表示已经弹出栈的元素的个数,j 表示已经进栈的总元素个数,那么f[ i ][ j ] 就表示在这个状态下的方案数( 注意:被弹出栈的元素的个数一定小于等于被压入栈中的元素的个数 )
  2. 我们可以 借用上一个递归思路的边界,去找这个递推思路的边界,我们假设,栈里面已经被压入了 j 个元素(由于是递推(当然也可以想成是dp,当前物品即为最后一个物品),已经没有其它的元素可以被压入栈里面了),而一个元素都没有被弹出,此时f[ 0 ][ j ] 的方案数为 1 ,这就是我们所学的递推边界。
  3. 知道了这些,我们还是没法写递推,我们首相要找到f[ i ][ j ] 状态是由那个状态 推导、变化 出来的。我们假设状态f[i - 1][ j ] 进栈了 j 个元素,出栈了 i - 1 个元素,那么此时再从栈里面弹出一个元素,就可以变化出 状态 f[ i ][ j ] ; 我们在做另一个假设状态 f[ i ][ j - 1] ,我们假设进栈了 j - 1个元素,出栈了 i 个元素,这个时候,如果在向栈里面 压入一个元素,那么 就是状态 f[ i ][ j ],所以f[ i ][ j ] 可以由两状态( f[i - 1][ j ] 、f[ i ][ j - 1])推到而来。
  4. 这样已经推倒完了?,还差一点细节,当状态f[ i ][ j ] 中 i == j 的时候,f[ i ][ j ] 只能够由状态 f[i - 1][ j] 推到而来,不能够由状态 f[ i ][ j - 1 ] 而来(因为此时 i == j 而出栈的元素 i 大于进栈元的个数 j - 1 ,这显然不符合逻辑
    到这里递推到就说完了,其实这里的递推,就像是个DP,那么下一个就开始讲用数论中 卡特兰 数列的方法解决这一题了。

3.卡特兰
他是数论中的动西,我也不太了解(所以我会简单的把这一题如何用卡特兰的方法给说一下

  • 思路如下:
  1. 首先我们仍是先建立数组 f[ ] ,f[ i ] 表示 i 个数的全部的组合的全部的可能性
  2. f[ 0 ] = 1 , f[ 1 ] = 1 //只有一个数方案数当然为1
    我们假设 x 元素是当前出栈序列的最后一个元素,x 有 n 中取值。
    由于x是最后一个出栈的,所以可以将已经出栈的数分成两部分:
    1.比x小的数
    2.比x大的数
    比x小的数有 x - 1 个,这些数的全部的出栈的可能为f[x - 2];
    比x大的数有 n - x 个,这些数的全部的出栈的可能为f[n - x].
    这两部分相互影响所以 f[ x ] = f[x - 2] * f[n - x] ;
    由于:x有n个取值,所以
    ans = f[0] * f[n - 1] + f[1] * f[n - 2] * + …+ f[n - 1] * f[0];

代码最后见。
4. 最后在附赠一个 高精度打表 出答案 ,这里的打表中用的式子是 卡特兰 式子来打表的

题解如下

//记忆化递归法
#include<iostream>
using namespace std;
const int Len = 20;
long long int f[Len][Len];

long long dfs(int i , int j)
{
    if(f[i][j]) return f[i][j];
    if(i == 0)  return 1;		//边界
    //对栈可以进行的两个操作
    if(j > 0)   f[i][j] += dfs(i , j - 1);	
    f[i][j] += dfs(i - 1 , j + 1);
    
    return f[i][j];
}

int main()
{
    freopen("T.txt","r",stdin);
    int n;
    scanf("%d", &n);
    long long ans = dfs(n , 0);
    printf("%lld",ans);

    return 0;
}

//递推/dp法
#include<iostream>
using namespace std;
const int Len = 20;
long long int f[Len][Len];        //f[i][j] 表示j个数进栈,i个数处栈(j >= j)

int main()
{
    //freopen("T.txt","r",stdin);
    int n;
    scanf("%d", &n);
    for(int i = 0;i <= n;i ++)
        f[0][i] = 1;            //预处理:i数进栈,0个数出栈,方案数为1
    for(int i = 1;i <= n;i ++)
        for(int j = i;j <= n;j ++)
            if(i == j)  f[i][j] = f[i - 1][j];      //当进栈的数字数 == 出栈的数字数的时候,只可能由f[i - 1][j](进栈了j个数出栈了 i - 1 个数)递推而来
            else
                f[i][j] = f[i - 1][j] + f[i][j -1];
    printf("%lld",f[n][n]);
    return 0;
}

//卡特兰
#include <cstdio>

int n, f[30];
int main()
{
    //递推实现卡特兰数
    scanf("%d", &n);
    f[0] = 1, f[1] = 1;
    for(int i=2; i<=n; i++)
        for(int j=0; j<i; j++)
            f[i] += f[j] * f[i-j-1];     //递推公式
    printf("%d", f[n]);
    return 0;
}

//高精 打表
#include<iostream>
#include<algorithm>
using namespace std;
const int Max = 100;    //这里我们假设所得的方案数 小于100位
int f[Max][Max] , c[Max];

int len(int a[])
{
    int i;
    for(i = Max - 10;i > 0;i --)
    {
        if(a[i] != 0)
            break;
    }
    return i;
}
//大数相加
void add(int a[], int b[], int w)
{
    int len_a = len(a);
    int len_b = len(b);
    int len_ab = max(len_a , len_b);
    for(int i = 0; i <= len_ab; i ++)
        f[w][i] = a[i] + b[i];
    for(int i = 0; i <= len_ab + 1; i ++)
    {
        f[w][i + 1] += f[w][i] / 10;
        f[w][i] %= 10;
    }
}

void catalan(int a[] , int b[])
{
    memset(c , 0 , sizeof(c));
    int len_a = len(a);
    int len_b = len(b);
    for(int i = 0; i <= len_a; i ++)        //高精度乘法
        for(int j = 0; j <= len_b; j ++)
            c[i + j] += a[i] * b[j];

    for(int i = 0; i <= len_a + len_b + 1; i ++)
    {
        c[i + 1] += c[i] / 10;
        c[i] %= 10;
    }
}

int main()
{
    f[0][0] = 1;
    f[1][0] = 1;

    for(int i = 2; i <= Max; i ++)      //要输出几,就让i 等于几
    {
        for(int j = 0; j < i; j ++)
        {
            catalan(f[j] , f[i - j - 1]);       //这两句等价于f[i] += f[j] * f[i - j - 1] ,只不过由于大数乘法的原因,变的复杂了
            add(f[i] , c , i);
        }
    }
    //打印数据
    for(int i = 1; i <= Max; i ++)
    {
        for(int j = len(f[i]); j >= 0; j --)
        {
            putchar((char)f[i][j] + '0');
        }
        printf("\n");
    }

    return 0;
}


  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值