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

引题:火车进出栈问题#

【题目大意】#

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

【关键词】#

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

【题解】#

Chapter1 -- 暴力出奇迹#

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

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

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

Copy
#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; }

Chapter2 -- 无脑递推#

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

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

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

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

  1. 1入栈
  2. 2,3,...,k"k1个数按某种顺序进出栈
  3. 1出栈
  4. k+1,k+2,...,NNk个数按某种顺序进出栈

于是这样就把原问题划分成了范围更小的子问题,得到公式:

f(n)=i=1Nf(k1)f(Nk)

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

时间复杂度为O(n2)

Chapter3 -- 状态转移#

看书去!《算法竞赛进阶指南 - 0x11P49P50

Chapter4 -- 玄学数论#

看书去! 这里我想重点讲讲。
从递推那里,其实可以看出点端倪了,如果你熟悉卡特兰数,你会发现这就是卡特兰数的定义式:

Catalann=i=0n1CatalaniCatalanni

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

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

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

易知总方案数为C2nn (想一想,为什么)。
我们现在要做的就是找到不合法的方案数。

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

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

为什么?我们接着证。

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

我们可以惊讶地发现不符合要求的方案与唯一一个n+10n11组成的排列一一对应。

所以不合法方案数为:C2nn1,当然,也可以是C2nn+1

然后抬公式:

Catalann=C2nnC2nn+1=(2n)!n!n!(2n)(n+1)!(n1)!=(2n)!(n+1)!n+1n(n1)!(2n)!(n+1)!(n1)!=(2n)!(n+1)!(n1)!(n+1n1)=(2n)!(n+1)!(n1)!1n=(2n)!(n+1)n!n!n1n=(2n)!n!n!1n+1=C2nnn+1

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

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

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

诶?爆内存!超时!

这是本题的第二个坑。

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

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

这时,思考唯一分解定理,即任意一个自然数都可分解且只能分解成以下形式:

n=p1k1p2k2p3k3...pmkm

其中,pi为质因数,ki为自然数。

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

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

Catalann=C2nnn+1=(2n)!n!n!(n+1)

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

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

怎么办呢?

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

先想,至少包含一个质因子p的个数是多少,显然是 np

那么,至少包含两个质因子p的个数是多少,显然是 np2

以此类推...

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

代码片段:

Copy
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 阶乘分解。

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

代码参上。

Copy
#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, ...

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

  • 定义式:Catalann=i=1n1CatalaniCatalanni
  • 通项式:Catalann=C2nnn+1
  • 另一通项式:Catalann=C2nnC2nn1=C2nnC2nn+1
  • 递推式:Catalann=Catalann12(2n1)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,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
  • ......

【参考文献】#

posted @   SilentEAG  阅读(1435)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示
CONTENTS