卡特兰数、Prüfer 序列、BSGS

1 卡特兰数

1.1 概述

卡特兰数的前几项是 1,1,2,5,14,42,132,429,1430,4862

卡特兰数在组合数学中有着许多应用。下面给出一个经典例子:

在网格中向右或向上走,从 (0,0) 走到 (n,n),并且不能越过对角线的路径条数。

该问题的结果就是卡特兰数,记为 Hn

1.2 通项公式

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

  1. (递归定义)Hn+1=H0Hn+H1Hn1++HnH0
  2. (递推公式)Hn=4n2n+1Hn1
  3. (通项公式)Hn=C2nnC2nn1=1n+1C2nn

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 序列是这样构造的:每次选择一个标号较小的叶子结点并将其删去,同时在序列中记录它所连接的那个节点。重复 n2 次后只剩下两个节点,算法结束。

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

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

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(nlogn)​ 的解法。

2.3.2 线性解法

发现两者的非线性解法出乎意料的一致,那么线性解法也是一致的。在此不再赘述。

2.4 Cayley 公式

n 个点的完全图有 nn2 颗生成树。

十分简洁的定理。如何证明呢?考虑 Prüfer 序列。任意一个长度为 n2 的值域 [1,n] 的整数序列都是原图生成树。因此方案数为 nn2

3 BSGS

3.1 概述

BSGS(baby step giant step),又叫大步小步算法。用于求解离散对数(即模意义下对数)的算法。也就是已知 axb(modm)(保证 a,m 互质),求 x

下面将一步步讲解 BSGS 算法。

3.2 算法内容

3.2.1 暴力法

既然 a,m 互质,我们直接想到欧拉定理。由于 aφ(m)1(modm)

也就是说,axm 的余数有一个长度为 φ(m) 的循环节。因此我们直接暴力枚举 [1,φ(m)]。最坏复杂度 O(m)

3.2.2 暴力优化

我们继续延续上面暴力法的思想。将 x 拆成 AtB,则原式就化为 aAtBb(modm)。即 aAtbaB(modm)。我们发现后面的的取值可以预处理。然后我们固定一个 t,计算出左边的可能取值。当这个值在右边出现过,AtB 就是所求 x

一般取 t=m 最合适,时间复杂度是 O(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 @   UKE_Automation  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示