8.14.16 ACM-ICPC 组合数学 图论计数
图论计数是组合数学中的一个重要分支,它研究图的各种性质及其计数方法。在ACM-ICPC比赛中,图论计数问题频繁出现,是选手们必须掌握的知识之一。本文将详细介绍图论计数的基本概念、常用方法及其在ACM-ICPC中的应用。
1. 图论计数的基本概念
图论计数主要研究图的各种组合性质,如顶点数、边数、路径数、环数等。一个图通常由顶点和边组成,根据边的性质可以分为有向图和无向图。
2. 图论计数的常用方法
2.1 手摇法
手摇法是一种基本的图论计数方法,主要用于计算图中的路径数和环数。通过枚举所有可能的路径或环并进行计数,可以得到准确的结果。这种方法适用于图的规模较小时使用。
2.2 递推关系
递推关系是图论计数中常用的技巧。通过建立顶点和边的递推关系,可以将复杂的计数问题转化为求解递推方程。例如,计算图中从一个顶点到另一个顶点的路径数时,可以利用动态规划的思想,将问题分解为子问题。
2.3 生成函数
生成函数是处理图论计数问题的强大工具。通过构造图的生成函数,可以将计数问题转化为求解生成函数的问题。生成函数在处理路径、环及连通子图计数问题时尤为有效。
3. 图论计数在ACM-ICPC中的应用
在ACM-ICPC比赛中,图论计数问题经常出现。以下是几个典型的应用场景:
3.1 最短路径计数
最短路径计数是图论中一个经典问题。通过Dijkstra算法或Floyd-Warshall算法可以找到最短路径,然后统计这些路径的数量。
3.2 环的计数
环的计数问题涉及找到图中所有环的数量。可以利用DFS(深度优先搜索)算法来找到所有环,并进行计数。
3.3 连通子图计数
连通子图计数是指计算图中所有连通子图的数量。可以通过DFS或BFS(广度优先搜索)遍历图,找到所有连通子图,并进行计数。
4. 图论计数的高级应用
除了基本应用,图论计数还可以用于解决一些复杂的问题,如:
4.1 复杂网络分析
在复杂网络中,图论计数可以用于分析网络的结构特性,如节点度分布、集群系数等。通过计数这些特性,可以了解网络的性质和行为。
4.2 生物信息学
在生物信息学中,图论计数可以用于分析生物网络,如蛋白质相互作用网络、基因调控网络等。通过计数这些网络中的各种组合特性,可以揭示生物系统的复杂关系。
4.3 社交网络分析
在社交网络分析中,图论计数可以用于研究社交网络的拓扑结构,如社区发现、影响力分析等。通过计数社交网络中的各种组合特性,可以帮助理解社交网络的动态行为。
5. 习题
- CF786D Dima and Bacteria
- 洛谷 P3957 [NOI2018] 连通子图
参考资料与注释
- 图论与网络流(Graph Theory and Network Flow)
结论
图论计数作为组合数学的重要分支,在ACM-ICPC比赛中有着广泛的应用。理解并掌握图论计数的基本方法和高级应用,可以帮助选手们更好地解决比赛中的复杂问题。希望本文对你理解图论计数有所帮助。
8.14.16 ACM-ICPC 组合数学 图论计数
在组合数学中,图论计数(Graph Enumeration)是研究满足特定性质的图的计数问题的分支。生成函数、波利亚计数定理、符号化方法和OEIS是解决这类问题时最重要的数学工具。图论计数可分为有标号和无标号两大类问题,大多数情况下有标号版本的问题都比其对应的无标号问题更加简单,因此我们将先考察有标号问题的计数。
1. 有标号树
即 Cayley 公式,参见 Prüfer 序列一文,我们也可以使用 Kirchhoff 矩阵树定理或生成函数和拉格朗日定理得到这一结果。
习题
- Hihocoder 1047. Random Tree
2. 有标号连通图
例题「POJ 1737」Connected Graph 题目大意:求有 nnn 个结点的有标号连通图的方案数(n≤50n \leq 50n≤50)。
这类问题最早出现于楼教主的男人八题系列中,我们设 gng_ngn 为 nnn 个点有标号图的方案数,cnc_ncn 为待求序列。nnn 个点的图至多有 (n2)\binom{n}{2}(2n) 条边,每条边根据其出现与否有两种状态,每种状态之间独立,因而有 gn=2(n2)g_n = 2^{\binom{n}{2}}gn=2(2n)。我们固定其中一个节点,枚举其所在连通块的大小,那么还需要从剩下的 n−1n-1n−1 个节点中选择 i−1i-1i−1 个节点组成一个连通块。连通块之外的节点可以任意连边,因而有如下递推关系:
移项得到 cnc_ncn 序列的 O(n2)O(n^2)O(n2) 递推公式,可以通过此题。
例题
- 集训队作业 2013:城市规划
题目大意:求有 nnn 个结点的有标号连通图的方案数(n≤130000n \leq 130000n≤130000)。对于数据范围更大的序列问题,往往我们需要构造这些序列的生成函数,以使用高效的多项式算法。
方法一:分治 FFT
上述的递推式可以看作一种自卷积形式,因而可以使用分治 FFT 进行计算,复杂度 O(nlog2n)O(n\log^2n)O(nlog2n)。
方法二:多项式求逆
我们将上述递推式中的组合数展开,并进行变形:
构造多项式:
代换进上式得到 CG=HCG = HCG=H,使用多项式求逆后再卷积解出 C(x)C(x)C(x) 即可。
方法三:多项式 exp
另一种做法是使用 EGF 中多项式 exp 的组合意义,我们设有标号连通图和简单图序列的 EGF 分别为 C(x)C(x)C(x) 和 G(x)G(x)G(x),那么它们将有下列关系:
使用多项式 ln 解出 C(x)C(x)C(x) 即可。
3. 有标号欧拉图、二分图
例题「SPOJ KPGRAPHS」Counting Graphs 题目大意:求有 nnn 个结点的分别满足下列性质的有标号图的方案数(n≤1000n \leq 1000n≤1000)。
- 连通图 A001187。
- 欧拉图 A033678。
- 二分图 A047864。
本题限制代码长度,因而无法直接使用多项式模板,但生成函数依然可以帮助我们进行分析。
连通图问题
在之前的例题中已被解决,考虑欧拉图。注意到上述对连通图计数的几种方法,均可以在满足任意性质的有标号连通图进行推广。例如我们可以将连通图递推公式中的 gng_ngn 从任意图替换成满足顶点度数均为偶数的图,此时得到的 cnc_ncn 即为欧拉图。
我们将 POJ 1737 的递推过程封装成连通化函数:
void ln(Int C[], Int G[]) {
for (int i = 1; i <= n; ++i) {
C[i] = G[i];
for (int j = 1; j <= i - 1; ++j)
C[i] -= binom[i - 1][j - 1] * C[j] * G[i - j];
}
}
前两问即可轻松解决:
for (int i = 1; i <= n; ++i) G[i] = pow(2, binom[i][2]);
ln(C, G);
for (int i = 1; i <= n; ++i) G[i] = pow(2, binom[i - 1][2]);
ln(E, G);
注意到这里的连通化递推过程其实等价于对其 EGF 求多项式 ln,同理我们也可以写出逆连通化函数,它等价于对其 EGF 求多项式 exp。
void exp(Int G[], Int C[]) {
for (int i = 1; i <= n; ++i) {
G[i] = C[i];
for (int j = 1; j <= i - 1; ++j)
G[i] += binom[i - 1][j - 1] * C[j] * G[i - j];
}
}
下面讨论有标号二分图计数。
二分图计数
我们设 bnb_nbn 表示 nnn 个结点的二分图方案数,gng_ngn 表示 nnn 个结点对结点进行 2 染色,满足相同颜色的结点之间不存在边的图的方案数。枚举其中一种颜色节点的数量,有:
接下来我们用两种不同的方法建立 gng_ngn 与 bnb_nbn 之间的关系。
方法一:算两次
我们设 cn,kc_{n, k}cn,k 表示有 kkk 个连通分量的二分图方案数,那么不难得到如下关系:
比较两种 gng_ngn 的表达式,展开得:
不难得到 bnb_nbn 的递推关系,复杂度 O(n3)O(n^3)O(n3),进一步使用容斥原理,可以优化到 O(n2)O(n^2)O(n2) 通过本题。
方法二:连通化递推
方法二和方法三均使用连通二分图 b1nb1_nb1n A001832 来建立 gng_ngn 与 bnb_nbn 之间的桥梁。注意到对于每个连通二分图,我们恰好有两种不同的染色方法,对应到两组不同的连通 2 染色图,因而对 gng_ngn 进行连通化,得到的序列恰好是 b1nb1_nb1n 的两倍,而 bnb_nbn 则由 b1nb1_nb1n 进行逆连通化得到。因此:
for (int i = 1; i <= n; ++i) {
G[i] = 0;
for (int j = 0; j < i + 1; ++j) G[i] += binom[i][j] * pow(2, j * (i - j));
}
ln(B1, G);
for (int i = 1; i <= n; ++i) B1[i] /= 2;
exp(B, B1);
两种递推的过程复杂度均为 O(n2)O(n^2)O(n2),可以通过本题。
方法三:多项式 exp
我们注意到也可以使用 EGF 理解上面的递推过程。设 G(x)G(x)G(x) 为 gng_ngn 的 EGF,B1(x)B1(x)B1(x) 为 b1nb1_nb1n 的 EGF,B(x)B(x)B(x) 为 bnb_nbn 的 EGF,应用做法二的方法,我们有:
我们可以对等式两边分别进行求导并比较两边系数,以得到易于编码的递推公式,通过此题。注意到做法二与做法三本质相同,且一般情况下做法三可以得到更优的时间复杂度。
习题
- UOJ Goodbye Jihai D. 新年的追逐战
- BZOJ 3864. 大朋友和多叉树
- BZOJ 2863. 愤怒的元首
- 洛谷 P6295. 有标号 DAG 计数
- LOJ 6569. 仙人掌计数
- LOJ 6570. 毛毛虫计数
- 洛谷 P5434. 有标号荒漠计数
- 洛谷 P3343. [ZJOI2015] 地震后的幻想乡
- HDU 5279. YJC plays Minecraft
- 洛谷 P7364. 有标号二分图计数
- 洛谷 P5827. 点双连通图计数
- 洛谷 P5827. 边双连通图计数
- 洛谷 P6596. How Many of Them
- 洛谷 U152448. 有标号强连通图计数
- Project Euler 434. Rigid graphs
无标号树
例题「SPOJ PT07D」Let us count 1 2 3 题目大意:求有 nnn 个结点的分别满足下列性质的树的方案数。
- 有标号有根树 A000169。
- 有标号无根树 A000272。
- 无标号有根树 A000081。
- 无标号无根树 A000055。
有根树
有标号情况在前文中解决,下面考察无标号有根树,设其 OGF 为 F(x)F(x)F(x),应用欧拉变换,可得:
取出系数即可。
无根树
考虑容斥,我们用有根树的方案中减去根不是重心的方案,并对 nnn 的奇偶性进行讨论。
当 nnn 是奇数时:
必然存在一棵子树大小 ≥⌈n2⌉\geq \left\lceil \frac{n}{2} \right\rceil≥⌈2n⌉,枚举这棵子树的大小有:
当 nnn 是偶数时:
注意到当有两个重心的情况时,上面的过程只会减去一次,因此还需要减去:
例题「洛谷 P5900」无标号无根树计数 题目大意:求有 nnn 个结点的无标号无根树的方案数(n≤200000n \leq 200000n≤200000)。对于数据范围更大的情况,做法同理,欧拉变换后使用多项式模板即可。
无标号简单图
例题「SGU 282. Isomorphism」 题目大意:求有 nnn 个结点的无标号完全图的边进行 mmm 染色的方案数。
注意到当 m=2m = 2m=2 时,所求对象就是无标号简单图 A000088,考察波利亚计数定理,
本题中置换群 GGG 为顶点的 nnn 阶对称群生成的边集置换群,但暴力做法的枚举量为 O(n!)O(n!)O(n!),无法通过此题。考虑根据置换的循环结构进行分类,每种循环结构对应一种数的分拆,我们用 dfs() 生成分拆,那么问题即转化为求每一种分拆 ppp 所对应的置换数目 w(p)w(p)w(p) 和每一类置换中的循环个数 c(p)c(p)c(p),答案为:
考虑 w(p)w(p)w(p),每一个分拆对应一个循环排列,同时同一种大小的分拆之间的顺序无关,因而我们有:
这里 qiq_iqi 表示大小为 iii 的分拆在 ppp 中出现的次数。
考虑 c(p)c(p)c(p),ppp 所影响的点集的循环即为 ∣p∣|p|∣p∣,但题目考察的是边染色,所以还需要考察点置换所生成的边置换,
如果一条边关联的顶点处在同一个循环内,设该循环大小为 pip_ipi,那么边所生成的循环数恰好为
参考代码
#include <bits/stdc++.h>
using namespace std;
#define Ts *this
#define rTs return Ts
typedef long long LL;
int MOD = int(1e9) + 7;
namespace NT {
void INC(int& a, int b) {
a += b;
if (a >= MOD) a -= MOD;
}
int sum(int a, int b) {
a += b;
if (a >= MOD) a -= MOD;
return a;
}
void DEC(int& a, int b) {
a -= b;
if (a < 0) a += MOD;
}
int dff(int a, int b) {
a -= b;
if (a < 0) a += MOD;
return a;
}
void MUL(int& a, int b) { a = (LL)a * b % MOD; }
int pdt(int a, int b) { return (LL)a * b % MOD; }
int _I(int b) {
int a = MOD, x1 = 0, x2 = 1, q;
while (1) {
q = a / b, a %= b;
if (!a) return x2;
DEC(x1, pdt(q, x2));
q = b / a, b %= a;
if (!b) return x1;
DEC(x2, pdt(q, x1));
}
}
void DIV(int& a, int b) { MUL(a, _I(b)); }
int qtt(int a, int b) { return pdt(a, _I(b)); }
inline int pow(int a, LL b) {
int c(1);
while (b) {
if (b & 1) MUL(c, a);
MUL(a, a), b >>= 1;
}
return c;
}
template <class T>
inline T pow(T a, LL b) {
T c(1);
while (b) {
if (b & 1) c *= a;
a *= a, b >>= 1;
}
return c;
}
template <class T>
inline T pow(T a, int b) {
return pow(a, (LL)b);
}
struct Int {
int val;
operator int() const { return val; }
Int(int _val = 0) : val(_val) {
val %= MOD;
if (val < 0) val += MOD;
}
Int(LL _val) : val(_val) {
_val %= MOD;
if (_val < 0) _val += MOD;
val = _val;
}
Int& operator+=(const int& rhs) {
INC(val, rhs);
rTs;
}
Int operator+(const int& rhs) const { return sum(val, rhs); }
Int& operator-=(const int& rhs) {
DEC(val, rhs);
rTs;
}
Int operator-(const int& rhs) const { return dff(val, rhs); }
Int& operator*=(const int& rhs) {
MUL(val, rhs);
rTs;
}
Int operator*(const int& rhs) const { return pdt(val, rhs); }
Int& operator/=(const int& rhs) {
DIV(val, rhs);
rTs;
}
Int operator/(const int& rhs) const { return qtt(val, rhs); }
Int operator-() const { return MOD - *this; }
};
} // namespace NT
using namespace NT;
const int N = int(1e3) + 9;
Int binom[N][N], C[N], E[N], B[N], B1[N], G[N];
int n;
void ln(Int C[], Int G[]) {
for (int i = 1; i <= n; ++i) {
C[i] = G[i];
for (int j = 1; j <= i - 1; ++j)
C[i] -= binom[i - 1][j - 1] * C[j] * G[i - j];
}
}
void exp(Int G[], Int C[]) {
for (int i = 1; i <= n; ++i) {
G[i] = C[i];
for (int j = 1; j <= i - 1; ++j)
G[i] += binom[i - 1][j - 1] * C[j] * G[i - j];
}
}
int main() {
#ifndef ONLINE_JUDGE
// freopen("in.txt", "r", stdin);
#endif
n = 1000;
for (int i = 0; i < n + 1; ++i) {
binom[i][0] = 1;
for (int j = 0; j < i; ++j)
binom[i][j + 1] = binom[i - 1][j] + binom[i - 1][j + 1];
}
for (int i = 1; i <= n; ++i) G[i] = pow(2, binom[i][2]);
ln(C, G);
for (int i = 1; i <= n; ++i) G[i] = pow(2, binom[i - 1][2]);
ln(E, G);
for (int i = 1; i <= n; ++i) {
G[i] = 0;
for (int j = 0; j < i + 1; ++j) G[i] += binom[i][j] * pow(2, j * (i - j));
}
ln(B1, G);
for (int i = 1; i <= n; ++i) B1[i] /= 2;
exp(B, B1);
int T;
cin >> T;
while (T--) {
scanf("%d", &n);
printf("Connected: %d\n", C[n]);
printf("Eulerian: %d\n", E[n]);
printf("Bipartite: %d\n", B[n]);
puts("");
}
}
习题
- CodeForces 438 E. The Child and Binary Tree
- 洛谷 P5448. [THUPC2018] 好图计数
- 洛谷 P5818. [JSOI2011] 同分异构体计数
- 洛谷 P6597. 烯烃计数
- 洛谷 P6598. 烷烃计数
- 洛谷 P4128. [SHOI2006] 有色图
- 洛谷 P4727. [HNOI2009] 图的同构计数
- AtCoder Beginner Contest 222 H. Binary Tree
- AtCoder Beginner Contest 284 Ex. Count Unlabeled Graphs
- 洛谷 P4708. 画画
- 洛谷 P7592. 数树(2021 CoE-II E)
- 洛谷 P5206. [WC2019] 数树
参考资料与注释
- WC2015, 顾昱洲营员交流资料 Graphical Enumeration
- WC2019, 生成函数,多项式算法与图的计数
- Counting labeled graphs - Algorithms for Competitive Programming
- Graphical Enumeration Paperback, Frank Harary, Edgar M. Palmer
- The encyclopedia of integer sequences, N. J. A. Sloane, Simon Plouffe
- Combinatorial Problems and Exercises, László Lovász
- Graph Theory and Additive Combinatorics