卡特兰数、Prüfer 序列、BSGS
1 卡特兰数
1.1 概述
卡特兰数的前几项是 \(1,1,2,5,14,42,132,429,1430,4862\cdots\)。
卡特兰数在组合数学中有着许多应用。下面给出一个经典例子:
在网格中向右或向上走,从 \((0,0)\) 走到 \((n,n)\),并且不能越过对角线的路径条数。
该问题的结果就是卡特兰数,记为 \(H_n\)。
1.2 通项公式
卡特兰数的通项公式有很多,下面给出几个常用公式:
- (递归定义)\(H_{n+1}=H_0H_n+H_1H_{n-1}+\cdots+H_nH_0\)。
- (递推公式)\(H_n=\dfrac{4n-2}{n+1}H_{n-1}\)。
- (通项公式)\(H_n=C_{2n}^n-C_{2n}^{n-1}=\dfrac1{n+1}C^{n}_{2n}\)。
1.3 应用
卡特兰数的特征是:一种操作数量时刻不能超过另一种操作数量,或者两种操作不能有交集。这样的方案数一般是卡特兰数。
下面举几个经典例子:
- 在网格中向右或向上走,从 \((0,0)\) 走到 \((n,n)\),并且不能越过对角线的路径条数。
- \(n\) 个元素以此进栈,合法的出栈序列。
- \(n\) 对括号的合法匹配方案。
- \(n\) 个节点构成的二叉树的形态数。
- 在圆上选择 \(2n\) 个点,将这些点成对连接起来使得所得到的 \(n\) 条线段不相交的方案数。
- 对角线不相交的情况下,将一个凸多边形区域分成三角形区域的方案数。
2 Prüfer 序列
2.1 概念
Prüfer 序列是一种将一颗有标号树用唯一一个序列表示的方法。
Prüfer 序列是这样构造的:每次选择一个标号较小的叶子结点并将其删去,同时在序列中记录它所连接的那个节点。重复 \(n-2\) 次后只剩下两个节点,算法结束。
根据分析可以直接得到一段 \(O(n\log n)\) 复杂度的代码。
下面我们讲解其优化以及其他应用。
2.2 线性构造
Prüfer 序列有一种 \(O(n)\) 构造的方式。如下所示:
- 维护一个指针 \(p\) 指向编号最小的叶子结点。
- 删除 \(p\) 指向的节点,并检查是否有新的叶节点产生。
- 如果产生,则比较新节点编号与 \(p\) 的关系,如果小于 \(p\) 就将该节点删除,检查其是否产生新叶子结点。重复上述步骤直到出现新节点编号大于 \(p\)。
- 让 \(p\) 增加,直到遇到一个新的叶子结点。
2.3 Prüfer 序列重建树
2.3.1 非线性解法
首先我们需要知道一条性质:每个结点在 Prüfer 序列中出现的次数是其度数减 \(1\)。
然后我们就能得到标号最小的叶子节点编号,与当前枚举到的序列的点连接,同时减掉两个点的度。最后我们会剩下两个点,将他们连接即可。
这还是一个 \(O(n\log n)\) 的解法。
2.3.2 线性解法
发现两者的非线性解法出乎意料的一致,那么线性解法也是一致的。在此不再赘述。
2.4 Cayley 公式
\(n\) 个点的完全图有 \(n^{n-2}\) 颗生成树。
十分简洁的定理。如何证明呢?考虑 Prüfer 序列。任意一个长度为 \(n-2\) 的值域 \([1,n]\) 的整数序列都是原图生成树。因此方案数为 \(n^{n-2}\)。
3 BSGS
3.1 概述
BSGS(baby step giant step),又叫大步小步算法。用于求解离散对数(即模意义下对数)的算法。也就是已知 \(a^x\equiv b\pmod{m}\)(保证 \(a,m\) 互质),求 \(x\)。
下面将一步步讲解 BSGS 算法。
3.2 算法内容
3.2.1 暴力法
既然 \(a,m\) 互质,我们直接想到欧拉定理。由于 \(a^{\varphi(m)}\equiv1\pmod{m}\)。
也就是说,\(a^x\) 模 \(m\) 的余数有一个长度为 \(\varphi(m)\) 的循环节。因此我们直接暴力枚举 \([1,\varphi(m)]\)。最坏复杂度 \(O(m)\)。
3.2.2 暴力优化
我们继续延续上面暴力法的思想。将 \(x\) 拆成 \(At-B\),则原式就化为 \(a^{At-B}\equiv b\pmod{m}\)。即 \(a^{At}\equiv ba^B\pmod{m}\)。我们发现后面的的取值可以预处理。然后我们固定一个 \(t\),计算出左边的可能取值。当这个值在右边出现过,\(At-B\) 就是所求 \(x\)。
一般取 \(t=\left\lceil\sqrt{m}\right\rceil\) 最合适,时间复杂度是 \(O(\sqrt{m})\)。
3.2.3 代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
unordered_map <int, int> hs;
void BSGS(int a, int b, int m) {
int cur = 1, t = sqrt(m) + 1;
for(int B = 1; B <= t; B++) {
cur = cur * a % m;
hs[cur * b % m] = B;
}
int now = cur;
for(int A = 1; A <= t; A++) {
auto it = hs.find(now);
if(it != hs.end()) {
cout << A * t - it->second;
return ;
}
now = now * cur % m;
}
cout << "no solution";
return ;
}
int p, b, n;
signed main() {
ios::sync_with_stdio(0);
cin >> p >> b >> n;
BSGS(b, n, p);
return 0;
}