8.14.16 ACM-ICPC 组合数学 图论计数

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(nlog⁡2n)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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值