卡特兰数、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 通项公式

卡特兰数的通项公式有很多,下面给出几个常用公式:

  1. (递归定义)\(H_{n+1}=H_0H_n+H_1H_{n-1}+\cdots+H_nH_0\)
  2. (递推公式)\(H_n=\dfrac{4n-2}{n+1}H_{n-1}\)
  3. (通项公式)\(H_n=C_{2n}^n-C_{2n}^{n-1}=\dfrac1{n+1}C^{n}_{2n}\)

1.3 应用

卡特兰数的特征是:一种操作数量时刻不能超过另一种操作数量,或者两种操作不能有交集。这样的方案数一般是卡特兰数。

下面举几个经典例子:

  1. 在网格中向右或向上走,从 \((0,0)\) 走到 \((n,n)\),并且不能越过对角线的路径条数。
  2. \(n\) 个元素以此进栈,合法的出栈序列。
  3. \(n\) 对括号的合法匹配方案。
  4. \(n\) 个节点构成的二叉树的形态数。
  5. 在圆上选择 \(2n\) 个点,将这些点成对连接起来使得所得到的 \(n\) 条线段不相交的方案数。
  6. 对角线不相交的情况下,将一个凸多边形区域分成三角形区域的方案数。

2 Prüfer 序列

2.1 概念

Prüfer 序列是一种将一颗有标号树用唯一一个序列表示的方法。

Prüfer 序列是这样构造的:每次选择一个标号较小的叶子结点并将其删去,同时在序列中记录它所连接的那个节点。重复 \(n-2\) 次后只剩下两个节点,算法结束。

根据分析可以直接得到一段 \(O(n\log n)\) 复杂度的代码。

下面我们讲解其优化以及其他应用。

2.2 线性构造

Prüfer 序列有一种 \(O(n)\) 构造的方式。如下所示:

  1. 维护一个指针 \(p\) 指向编号最小的叶子结点。
  2. 删除 \(p\) 指向的节点,并检查是否有新的叶节点产生。
  3. 如果产生,则比较新节点编号与 \(p\) 的关系,如果小于 \(p\) 就将该节点删除,检查其是否产生新叶子结点。重复上述步骤直到出现新节点编号大于 \(p\)
  4. \(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;
}
posted @ 2024-02-27 17:52  UKE_Automation  阅读(25)  评论(0编辑  收藏  举报