zoukankan      html  css  js  c++  java
  • 【讲●解】火车进出栈类问题 & 卡特兰数应用

    引题:火车进出栈问题

    【题目大意】

    给定 (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("
    ");
            */
            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})

    然后抬公式:

    (egin{equation} egin{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)为自然数。

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

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

    (egin{equation} egin{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=sum_{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,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
    • ......

    【参考文献】

  • 相关阅读:
    Django模型层Meta内部类详解
    jquery checkbox的相关操作——全选、反选、获得所有选中的checkbox
    c# 委托与异步调用
    DataTable转成List集合
    c# winform 自动升级
    C# winform单元格的formatted值的类型错误 DataGridView中CheckBox列运行时候System.FormatException异常
    C#创建无窗体的应用程序
    sql 一个表的字段更新至另一个字段的方法
    datagridview 获取选中行的索引
    CHECKEDLISTBOX用法总结
  • 原文地址:https://www.cnblogs.com/silentEAG/p/10439166.html
Copyright © 2011-2022 走看看