引题:火车进出栈问题

【题目大意】

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

【参考文献】

【讲●解】火车进出栈类问题 & 卡特兰数应用的更多相关文章

  1. luogu P1044 火车进出栈问题(Catalan数)

    Catalan数就是魔法 火车进出栈问题即: 一个栈(无穷大)的进栈序列为 1,2,3,4,...,n 求有多少个不同的出栈序列? 将问题进行抽象, 假设'+'代表进栈, 则有'-'代表出栈 那么如果 ...

  2. 出栈顺序 与 卡特兰数(Catalan)的关系

    一,问题描述 给定一个以字符串形式表示的入栈序列,请求出一共有多少种可能的出栈顺序?如何输出所有可能的出栈序列? 比如入栈序列为:1 2 3  ,则出栈序列一共有五种,分别如下:1 2 3.1 3 2 ...

  3. CH1102 火车进出栈问题(高精/卡特兰数)

    描述 一列火车n节车厢,依次编号为1,2,3,-,n.每节车厢有两种运动方式,进栈与出栈,问n节车厢出栈的可能排列方式有多少种. 输入格式 一个数,n(n<=60000) 输出格式 一个数s表示 ...

  4. 火车进出栈 java

    题目描述 一列火车n节车厢,依次编号为1,2,3,…,n.每节车厢有两种运动方式,进栈与出栈,问n节车厢出栈的可能排列方式有多少种. 输入 一个数,n(n<=60000) 输出 一个数s表示n节 ...

  5. [NOIP2003]栈 题解(卡特兰数)

    [NOIP2003]栈 Description 宁宁考虑的是这样一个问题:一个操作数序列,从1,2,一直到n(图示为1到3的情况),栈A的深度大于n. 现在可以进行两种操作: 1.将一个数,从操作数序 ...

  6. SDUT 1266 出栈序列统计(卡特兰数)

    这道题是回溯算法,网上一查是卡特兰数先占上代码,题解过两天会写. #include <bits/stdc++.h> using namespace std; int main() { // ...

  7. tyvj/joyoi 1374 火车进出栈问题(水水版)

    我受不了了. Catalan数第100项,30000项,50000项,cnm 这tm哪里是在考数学,分明是在考高精度,FFT...... 有剧毒! 我只得写高精度,只能过100的那个题,两个进化版超时 ...

  8. vijos - P1122出栈序列统计 (卡特兰数)

    P1122出栈序列统计 未递交 标签:NOIP普及组2003[显示标签] 描写叙述 栈是经常使用的一种数据结构,有n令元素在栈顶端一側等待进栈,栈顶端还有一側是出栈序列. 你已经知道栈的操作有两·种: ...

  9. 卡特兰数Catalan——定义、公式、模型总结

    推荐:卡特兰数总结 定义: f(i)表示,从(0,0)出发,到(i,i),每次只能向上或者向右走,并且不越过红线的方案数. 这个图片的点上的数字,其实告诉我们f[i],就可以根据这个n方dp得到. 其 ...

随机推荐

  1. ChartCtrl源码剖析之——CChartGrid类

    CChartGrid类用来绘制波形区域中的表格,当绘制波形时波形就显示在这些表格上面.它处于该控件的区域,如下图所示: CChartGrid类的头文件. #if !defined(AFX_CHARTG ...

  2. <hr />标签的颜色和粗细设定

    设置<hr />的颜色和粗细,不需要用到style,直接用标签的color和size属性: <hr color="red" size="0.5" ...

  3. poj2676 Sudoku(搜索)

    题目链接:http://poj.org/problem?id=2676 题意:9*9的方格,0代表没数字,其他代表数字,请在格子中填入1~9的数字,使得在每行,每列和每个3*3的方块中,1~9的数字每 ...

  4. bzoj 4555: [Tjoi2016&Heoi2016]求和【NTT】

    暴力推式子推诚卷积形式,但是看好多blog说多项式求逆不知道是啥.. \[ \sum_{i=0}^{n}\sum_{j=0}^{n}S(i,j)*2^j*j! \] \[ S(i,j)=\frac{1 ...

  5. 洛谷P3261 [JLOI2015]城池攻占(左偏树)

    传送门 每一个城市代表的点开一个小根堆,把每一个骑士合并到它开始攻占的城池所代表的点上 然后开始dfs,每一次把子树里那些还活着的骑士合并上来 然后再考虑当前点的堆,一直pop直到骑士全死光或者剩下的 ...

  6. ios手机Safari本地服务连不上

    问题: 今天在本地起服务准备测下ios手机端页面,结果发现:页面可以打开,但是登录不上. 用alert定位了下,await fn() 报错被try()catch(){}捕获了... 原因: 该机子不支 ...

  7. 解决window.opener.obj instanceof Object会输出false的问题

    在a.html页面中: window.obj = {name: "jinbang"} 在a.html页面中打开新窗口b.html页面: console.log(window.ope ...

  8. PHP简单实现单点登录功能示例

    1.准备两个虚拟域名 127.0.0.1  www.openpoor.com127.0.0.1  www.myspace.com 2.在openpoor的根目录下创建以下文件 index.PHP 1 ...

  9. python删除列表中元素的方法

    删除列表中元素的三种方法-remove.pop.del 1 1.remove: 删除单个元素,删除首个符合条件的元素,按值删除 2 举例说明: 3 >>> str=[1,2,3,4, ...

  10. 题解报告:hdu1205吃糖果(插空法)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1205 Problem Description HOHO,终于从Speakless手上赢走了所有的糖果, ...