引题:火车进出栈问题
【题目大意】
给定 (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("
");
*/
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})。
然后抬公式:
(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)呢......
位数那么高,写个组合数计算+高精乘+高精除,就算是压位高精,也过不了呀。。。(亲测)
亲亲呢,这边建议您分解质因数呢。
这时,思考唯一分解定理,即任意一个自然数都可分解且只能分解成以下形式:
其中,(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,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
- ......
【参考文献】
- Catalan number--维基百科(这个真建议看下)
- 卡特兰数--百度百科
- 卡特兰数公式推导(母函数)
- 卡特兰数应用详讲
- 《算法竞赛进阶指南》
- 《算法导论》