【讲●解】火车进出栈类问题 & 卡特兰数应用

火车进出栈类问题详讲 & 卡特兰数应用


引题:火车进出栈问题

【题目大意】

给定 \(1\)~\(N\)\(N\)个整数和一个大小无限的栈,每个数都要进栈并出栈一次。如果进栈的顺序为 \(1,2,3,...,N\),那么可能的出栈序列有多少种?

【关键词】

  • 栈的思想
  • 算法优化
  • 卡特兰数 (Catalan number)

【题解】

\(\mathfrak{Chapter1}\) -- 暴力出奇迹

首先,从状态的角度出发思考,每一层解答树都有两个分支:

  1. 把下一个数进栈。
  2. 把当前栈顶的数出栈(如果栈顶非空)。

用递归实现的话,因为解答树有 \(N\) 层,每层产生\(2\)个分支,所以时间复杂度为\(O(2^n)\)

#include<cstdio>
#include<cstdlib>
#define MAX 60000 + 5
int n, c[MAX], a[MAX], top, cnt, num;
inline void dfs(int s) {
    if(cnt == n) { // 到达结束条件
        num++; // 统计 
        /*
        for (int i = 1;i <= n; ++i) printf("%d", c[i]);
        printf("\n");
        */
        return;
    } 
    if (top > 0) { // 如果当前栈顶有数,就弹出,生成下一层解答树 
        int tp = a[top--];
        c[++cnt] = tp;
        dfs(s);
        a[++top] = tp; // 还原现场 
        cnt--; 
    }
    if (s <= n) { // 把下一个数进栈 ,生成下一层解答树  
        a[++top] = s;
        dfs(s+1);
        top--;
    }
}
int main(){
    scanf("%d", &n);
    dfs(1); // 解答树(搜索树) 
    printf("%d", num);
    return 0;
}

\(\mathfrak{Chapter2}\) -- 无脑递推

曾经有一道题需要我们求出\(N\)层汉罗塔从\(A\)柱移动到\(C\)柱最少的步数。当时我们是怎么做的?

\(f(n)\)表示\(N\)层汉罗塔从\(A\)柱移动到\(B\)柱最少的步数,我们想,先把上面的\(N-1\)个木块移动到\(B\)柱,再将最后的一个木块移动到\(C\)柱,最后将\(B\)柱上的\(N-1\)个木块移动到\(C\)柱。这样,就能得到递推方程:\(f(n)=2*f(n-1)+1\)

现在,我们同样从递推的角度思考这个问题。

\(f(n)\)表示进栈顺序为\(1,2,...,N\)时可能的出栈方案数,根据以前的经验,我们需要把它划分成范围更小的子问题。
考虑\(“1”\)这个数排在最终序列的位置,可知只要\(“1”\)的位置不同,序列就不同。如果\(“1”\)这个数排在第\(k\)个,那么整个序列进出栈的过程即为:

  1. \(“1”\)入栈
  2. \(“2,3,...,k"\)\(k-1\)个数按某种顺序进出栈
  3. \(“1”\)出栈
  4. \(“k+1,k+2,...,N”\)\(N-k\)个数按某种顺序进出栈

于是这样就把原问题划分成了范围更小的子问题,得到公式:
\[ f(n)=\sum_{i=1}^{N}f(k-1)*f(N-k) \]

当然,边界条件为:\(f(0)=1,f(1)=1\)

时间复杂度为\(O(n^2)\)

\(\mathfrak{Chapter3}\) -- 状态转移

看书去!《算法竞赛进阶指南 - \(0x11\)\(P49-P50\)

\(\mathfrak{Chapter4}\) -- 玄学数论

看书去! 这里我想重点讲讲。
从递推那里,其实可以看出点端倪了,如果你熟悉卡特兰数,你会发现这就是卡特兰数的定义式:
\[ Catalan_n=\prod_{i=0}^{n-1}Catalan_i*Catalan_{n-i} \]

当然,如果直接用这个数学公式,时间开销也是接受不了的,我们需要推导出一个比它更优美的通项公式。

火车进出栈这个问题可以进一步抽象化。
我们用\(“0”\)表示出栈,\(“1”\)表示入栈,该题即等价于:
\(n\)\(1\)\(n\)\(0\)组成的\(2n\)位的二进制数,要求从左到右扫描,\(1\)的累计数不小于0的累计数,试求满足这条件的数有多少?

首先,直接找合法方案数肯定不好找,所以考虑总方案数减去不合法方案数。

易知总方案数为\(C_{2n}^{n}\) (想一想,为什么)。
我们现在要做的就是找到不合法的方案数。

思考:不合法的方案满足什么条件?
从左往右扫时,必然在某一奇数位\(2p+1\)上首先出现\(p+1\)\(0\),和\(p\)\(1\)。(反证法可以证明滴)

此后的\([2p+2,2n]\)上的\(2n-(2p+1)\)位有\(n-p\)\(1\),\(n-p-1\)\(0\)。如若把后面这部分\(2n-(2p+1)\)位的\(0\)\(1\)互换,使之成为\(n-p\)\(0\)\(n-p-1\)\(1\),结果得 \(1\)个由\(n+1\)\(0\)\(n-1\)\(1\)组成的\(2n\)位数,即一个不合法的方案对应着一个由\(n-1\)\(1\)\(n+1\)\(0\)组成的一个排列

为什么?我们接着证。

任意一个由\(n-1\)\(1\)\(n+1\)\(0\)组成的一个排列,因为\(0\)的个数多了\(2\)个,且\(2n\)为偶数,所以必定在奇数位\(2p+1\)上出现\(0\)的个数超过\(1\)的个数。同样把后面部分\(0\)\(1\)互换。使之成为由\(n\)\(0\)\(n\)\(1\)组成的\(2n\)位数。

我们可以惊讶地发现不符合要求的方案与唯一一个\(n+1\)\(0\)\(n-1\)\(1\)组成的排列一一对应。

所以不合法方案数为:\(C_{2n}^{n-1}\),当然,也可以是\(C_{2n}^{n+1}\)

然后抬公式:

\(\begin{equation} \begin{aligned} Catalan_n&=C_{2n}^{n}-C_{2n}^{n+1} \\ &=\frac{(2n)!}{n!n!}-\frac{(2n)}{(n+1)!(n-1)!}\\ &=\frac{(2n)!}{\frac{(n+1)!}{n+1}*n(n-1)!}-\frac{(2n)!}{(n+1)!(n-1)!}\\ &=\frac{(2n)!}{(n+1)!*(n-1)!}*(\frac{n+1}{n}-1)\\ &=\frac{(2n)!}{(n+1)!*(n-1)!}*\frac{1}{n}\\ &=\frac{(2n)!}{(n+1)n!*\frac{n!}{n}}*\frac{1}{n}\\ &=\frac{(2n)!}{n!n!}*\frac{1}{n+1}\\ &=\frac{C_{2n}^{n}}{n+1} \end{aligned} \end{equation}\)

这就是卡特兰数的通项公式

从推导来说,通项公式与定义式等价,但,,,,如果想从数学角度证明这两个式子的等价性,,,,就得用到母函数的相关知识QAQ。这题不是数论题啊。。

不管这些,然后我们就可以愉快地用卡特兰数的通项公式解题了。

诶?爆内存!超时!

这是本题的第二个坑。

看一眼数据范围......\(n<=60000\)呢......
位数那么高,写个组合数计算+高精乘+高精除,就算是压位高精,也过不了呀。。。(亲测)

亲亲呢,这边建议您分解质因数呢。

这时,思考唯一分解定理,即任意一个自然数都可分解且只能分解成以下形式:
\[ n=p_1^{k_1}*p_2^{k_2}*p_3^{k_3}*...*p_m^{k_m} \]

其中,\(p_i\)为质因数,\(k_i\)为自然数。

这样,我们就可以把分子分母各自的质因数和其相应的指数求出来,一一约掉,大大减少时间开销。我怎么没想到呢

为了约得方便(明明是想偷懒),我们再把通项公式变个形。

\(\begin{equation} \begin{aligned} Catalan_n&=\frac{C_{2n}^{n}}{n+1}\\ &=\frac{(2n)!}{n!n!(n+1)} \end{aligned} \end{equation}\)

这里又有一个问题。
如果一个数为\(n\),要我们求它的唯一分解式,这很好办啊,直接先筛一遍质数,然后一一枚举质数是否被该数整除,如果是,就枚举该质数的指数。然后就求出来了。

但,这道题的“数”是一个阶乘。

怎么办呢?

一个一个分解显然不可行,我们考虑对于每个质数,计算它在\(1×2×...×N\)中每个数分解质因数后对应的指数和。

先想,至少包含一个质因子\(p\)的个数是多少,显然是 \(⌊\frac{n}{p}⌋\)

那么,至少包含两个质因子\(p\)的个数是多少,显然是 \(⌊\frac{n}{p^2}⌋\)

以此类推...

由于包含\(x\)个质因子的数中的前\(x−1\)个质因子已经在之前的情况中统计过,只需要累加当前结果就可以了。

代码片段:

    for (int i = 1;i <= cnt; ++i) {
        if (prim[i] > n) break;
        int sum = 0;
        for (int j = prim[i];j <= n; j = j*prim[i]) sum += n/j;
        num[prim[i]] = sum;
        }

真巧,这里有道题,顺便还可以把这道题给\(A\)了。(买一赠一)
CH3101 阶乘分解。

高精的事,,,能叫事吗 就不说了吧,,,
然后,就可以完美地解决这道 放在数据结构里的 数论题了。

代码参上。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
#define R register
using namespace std;
const int MAX = 120000 + 5;
const int SIZE = 5500;
inline int read(){
    int f = 1, x = 0;char ch;
    do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0'||ch>'9');
    do {x = x*10+ch-'0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
    return f*x;
}
int n; 
int vis[MAX], prim[MAX], cnt; //筛质数 
int mol_p[MAX];//分子的p 
int mol_k[MAX];//分子的k 
int mol_cnt;//分子计数 
int pos;//记录分子质因数最高到哪里 
int f[MAX];//映射数组 

/*
封装式高精 
*/
const int base = 1e8;
const int N = 1e4 + 10;
struct bigint {
    int s[N], l;
    void CL() { l = 0; memset(s, 0, sizeof(s)); }
    void pr() {
        printf("%d", s[l]);
        for (int i = l - 1; i; i--)
            printf("%08d", s[i]);
    }
    bigint operator = (ll b)
    {
        CL();
        do
        {
            s[++l] = b % base;
            b /= base;
        } while (b > 0);
        return *this;
    }
    bigint operator * (bigint &b)
    {
        bigint c;
        ll x;
        int i, j, k;
        c.CL();
        for (i = 1; i <= l; i++)
        {
            x = 0;
            for (j = 1; j <= b.l; j++)
            {
                x = x + 1LL * s[i] * b.s[j] + c.s[k = i + j - 1];
                c.s[k] = x % base;
                x /= base;
            }
            if (x)
                c.s[i + b.l] = x;
        }
        for (c.l = l + b.l; !c.s[c.l] && c.l > 1; c.l--);
        return c;
    }
    bigint operator * (const ll &b)
    {
        bigint c;
        if (b > 2e9)
        {
            c = b;
            return *this * c;
        }
        ll x = 0;
        c.CL();
        for (int i = 1; i <= l; i++)
        {
            x = x + b * s[i];
            c.s[i] = x % base;
            x /= base;
        }
        for (c.l = l; x; x /= base)
            c.s[++c.l] = x % base;
        return c;
    }
    bool operator < (const bigint &b) const
    {
        if (l ^ b.l)
            return l < b.l;
        for (int i = l; i; i--)
            if (s[i] ^ b.s[i])
                return s[i] < b.s[i];
        return false;
    }
};

inline void ola(int limit) { //披着欧拉筛的线性筛 
    memset(vis, 0, sizeof(vis));
    cnt = 0;
    vis[1] = 1;
    for (R int i = 2;i <= limit; ++i) {
        if (vis[i] == 0) {
            prim[++cnt] = i;
            vis[i] = i;
        }   
        for (R int j = 1;j <= cnt ; ++j) {
            if(prim[j] > vis[i] || prim[j] > MAX / i) break;
            vis[i*prim[j]] = prim[j];
        }
    }
}
 
inline ll pows(ll a,ll b){//快速幂 
    ll ans=1;
    while(b){
        if(b&1)ans *= a;
        a *= a,b >>= 1;
    }
    return ans;
}
 
int main(){
    n = read();
    int m = 2*n; int s = n+1; //看上面公式就明白了 
    ola(m);//线性筛 
    pos = cnt;//记录下 
    for (int i = 1;i <= cnt; ++i) { //分解分子并存入相应的数组,f数组用来作一次映射 
        if (prim[i] > m) break;
        int sum = 0;
        for (int j = prim[i];j <= m; j = j*prim[i]) sum += m/j;
        mol_p[++mol_cnt] = prim[i], mol_k[mol_cnt] = sum;
        f[prim[i]] = mol_cnt;
    }
    for (int i = 1;i <= cnt; ++i) {//分解分母中的n!n! 
        if (prim[i] > n) break;
        int sum = 0;
        for (int j = prim[i];j <= n; j = j*prim[i]) sum += n/j;
        mol_k[f[prim[i]]] -= (sum + sum);//因为2个n!,一起约掉 
    }
    for (int i = 1;i <= cnt; ++i) {//分解分母中的n+1 
        if (prim[i] > s) break;
        if (s % prim[i] == 0) {
            int sum = 0;
            while (s % prim[i] == 0) {
                sum++;
                s /= prim[i];
            }
            mol_k[f[prim[i]]] -= sum;
        }
    }   
    bigint ans;
    ans = 1;
    for (int i = 1;i <= pos; ++i) { //把剩余的相乘 
        ans = ans * pows(mol_p[i], mol_k[i]);
    }
    ans.pr();//高精输出 
    return 0;
}

莫名觉得自己讲的有点跑题,,,明明是数据结构呢,,,

【补充:浅谈卡特兰数】

1.关于卡特兰数

以下内容摘自百度百科

  • 卡特兰数又称卡塔兰数,英文名Catalan number,是组合数学中一个常出现在各种计数问题中出现的数列。以比利时的数学家欧仁·查理·卡特兰 (1814–1894)的名字来命名,其前几项为(从第零项开始) :
    1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, ...

简单的说,卡特兰数就是一个如同斐波拉契数列一样的数列。我们用\(Catalan_n\)表示第\(n\)位的卡特兰数,令\(Catalan_0=1,Catalan_1=1\)\(catalan\)数满足以下特性:

  • 定义式:\(Catalan_n=\prod_{i=1}^{n-1}Catalan_i*Catalan_{n-i}\)
  • 通项式:\(Catalan_n=\frac{C_{2n}^{n}}{n+1}\)
  • 另一通项式:\(Catalan_n=C_{2n}^{n}-C_{2n}^{n-1}=C_{2n}^{n}-C_{2n}^{n+1}\)
  • 递推式:\(Catalan_n=\frac{Catalan_{n-1}*2(2*n-1)}{n+1}\)

实质上都是等价式

2.公式等价证明

略略略,,有兴趣的话看下面参考文献!

数学功底要好啊

3.应用

  • 求满二叉树有多少种结构。
  • 在一个凸多边形中,通过若干条互不相交的对角线,把这个多边形划分成了若干个三角形。任务是键盘上输入凸多边形的边数n,求不同划分的方案数f(n)。
  • 在n*n的格子中,只在下三角行走,每次横或竖走一格,有多少种走法。
  • 在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数。
  • n个长方形填充一个高度为n的阶梯状图形的方法个数。
  • 有2n个人排成一行进入剧场。入场费5元。其中只有n个人有一张5元钞票,另外n人只有10元钞票,剧院无其它钞票,问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)。
  • 12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种。
  • 括号化问题。矩阵链乘: P=A1×A2×A3×……×An,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
  • ......

    【参考文献】

  • Catalan number--维基百科(这个真建议看下)
  • 卡特兰数--百度百科
  • 卡特兰数公式推导(母函数)
  • 卡特兰数应用详讲
  • 《算法竞赛进阶指南》
  • 《算法导论》

转载于:https://www.cnblogs.com/silentEAG/p/10439166.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值