【讲●解】火车进出栈类问题 & 卡特兰数应用
引题:火车进出栈问题
【题目大意】
给定 \(1\)~\(N\) 这\(N\)个整数和一个大小无限的栈,每个数都要进栈并出栈一次。如果进栈的顺序为 \(1,2,3,...,N\),那么可能的出栈序列有多少种?
【关键词】
- 栈的思想
- 算法优化
- 卡特兰数 (Catalan number)
【题解】
\(\mathfrak{Chapter1}\) -- 暴力出奇迹
首先,从状态的角度出发思考,每一层解答树都有两个分支:
- 把下一个数进栈。
- 把当前栈顶的数出栈(如果栈顶非空)。
用递归实现的话,因为解答树有 \(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”\)入栈。
- \(“2,3,...,k"\)这\(k-1\)个数按某种顺序进出栈。
- \(“1”\)出栈。
- \(“k+1,k+2,...,N”\)这\(N-k\)个数按某种顺序进出栈。
于是这样就把原问题划分成了范围更小的子问题,得到公式:
当然,边界条件为:\(f(0)=1,f(1)=1\)
时间复杂度为\(O(n^2)\)。
\(\mathfrak{Chapter3}\) -- 状态转移
看书去!《算法竞赛进阶指南 - \(0x11\)》\(P49-P50\)
\(\mathfrak{Chapter4}\) -- 玄学数论
看书去! 这里我想重点讲讲。
从递推那里,其实可以看出点端倪了,如果你熟悉卡特兰数,你会发现这就是卡特兰数的定义式:
当然,如果直接用这个数学公式,时间开销也是接受不了的,我们需要推导出一个比它更优美的通项公式。
火车进出栈这个问题可以进一步抽象化。
我们用\(“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\)呢......
位数那么高,写个组合数计算+高精乘+高精除,就算是压位高精,也过不了呀。。。(亲测)
亲亲呢,这边建议您分解质因数呢。
这时,思考唯一分解定理,即任意一个自然数都可分解且只能分解成以下形式:
其中,\(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,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
- ......
【参考文献】
- Catalan number--维基百科(这个真建议看下)
- 卡特兰数--百度百科
- 卡特兰数公式推导(母函数)
- 卡特兰数应用详讲
- 《算法竞赛进阶指南》
- 《算法导论》