In combinatorics, graph enumeration (Graph Enumeration) is the branch of combinatorics concerned with counting graphs satisfying specified properties. Generating functions, the Pólya enumeration theorem, the symbolic method, and OEIS are among the most important tools for these problems. Graph enumeration is divided into labeled and unlabeled problems. In most cases1, labeled problems are simpler than their unlabeled counterparts, so we begin with labeled graphs.
Labeled Trees
This is Cayley's formula; see the Prüfer sequence. We can also use Kirchhoff's matrix-tree theorem, generating functions, and Lagrange's theorem to obtain the same result.
Exercises
Labeled Connected Graphs
Example: POJ 1737, Connected Graph
Example: POJ 1737, Connected Graph
Problem statement: count the number of labeled connected graphs on vertices ().
This type of problem first appeared in Lou Zhiming's Manba Eight Problems series. Let be the number of labeled graphs on vertices, and let be the sequence to be found. A graph on vertices has at most edges; each edge has two independent states, present or absent, so . Fix one vertex and enumerate the size of its connected component. We then choose of the remaining vertices to form a connected component. Vertices outside that component may be joined arbitrarily, giving the following recurrence:
Rearranging gives an recurrence for the sequence , which solves the problem.
Example: CTSC 2013, Urban Planning
Example: CTSC 2013, Urban Planning
Problem statement: count the number of labeled connected graphs on vertices ().
For sequence problems with larger limits, we usually construct generating functions for the sequences so that efficient polynomial algorithms can be used.
Method 1: Divide-and-Conquer FFT
The recurrence above can be viewed as a self-convolution, so it can be computed with divide-and-conquer FFT in time.
Method 2: Polynomial Inversion
Expand the binomial coefficients in the recurrence above and transform it:
Construct the polynomials:
Substitution into the equation above gives . Use polynomial inversion and then convolution to solve for .
Method 3: Polynomial exp
Another approach is to use the combinatorial meaning of polynomial exp in EGFs. Let and be the EGFs of labeled connected graphs and simple graphs, respectively. Then:
Use polynomial ln to solve for .
Labeled Eulerian and Bipartite Graphs
Example: SPOJ KPGRAPHS, Counting Graphs
Example: SPOJ KPGRAPHS, Counting Graphs
Problem statement: count the number of labeled graphs on vertices satisfying each of the following properties ().
The code length is limited in this problem, so a polynomial template cannot be used directly; generating functions can still help us analyze it.
The connected-graph problem was solved in the previous example; now consider Eulerian graphs. The methods above for counting connected graphs can all be generalized to labeled connected graphs satisfying an arbitrary property. For example, replace in the connected-graph recurrence, which counts arbitrary graphs, with the number of graphs whose vertex degrees are all even. The resulting counts Eulerian graphs.
We encapsulate the recurrence from POJ 1737 as a connected-component function:
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];
}
}
The first two questions are then easy to solve:
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);
The connected-component recurrence here is equivalent to taking the polynomial ln of its EGF. Similarly, we can write an inverse connected-component function, which is equivalent to taking the polynomial exp of its EGF.
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];
}
}
We next discuss counting labeled bipartite graphs.
Let denote the number of bipartite graphs on vertices, and let denote the number of ways to 2-color the vertices of an n-vertex graph so that no edge joins vertices of the same color. Enumerating the number of vertices of one color gives2:
We next establish the relationship between and in two different ways.
Method 1: Count Twice
Let denote the number of bipartite graphs with k connected components. We readily obtain:
Comparing the two expressions for and expanding gives:
It follows that the recurrence for has complexity . Applying inclusion-exclusion further reduces it to , enough to solve the problem.
Method 2: Connected-Component Recurrence
Methods 2 and 3 both use the connected bipartite graph sequence A001832 to connect and .
Every connected bipartite graph has exactly two distinct 2-colorings, corresponding to two connected 2-colored graphs. Therefore, applying connected-component decomposition to produces exactly twice the sequence , while is obtained from by inverse connected-component decomposition.
Therefore:
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);
Both recurrences have complexity , so either solves the problem.
Method 3: Polynomial exp
We can also use EGFs to understand the recurrence above.
Let , , and be the EGFs of , , and , respectively. Applying Method 2 gives:
Differentiate both sides of the equation and compare coefficients to obtain an easy-to-code recurrence that solves the problem. Methods 2 and 3 are essentially the same, and Method 3 generally has better time complexity.
Reference code
#include <iostream>
using namespace std;
using LL = long long;
constexpr int MOD = int(1e9) + 7;
// <<= '2. Number Theory .,//{
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)); }
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>
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>
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);
return *this;
}
Int operator+(const int& rhs) const { return sum(val, rhs); }
Int& operator-=(const int& rhs) {
DEC(val, rhs);
return *this;
}
Int operator-(const int& rhs) const { return dff(val, rhs); }
Int& operator*=(const int& rhs) {
MUL(val, rhs);
return *this;
}
Int operator*(const int& rhs) const { return pdt(val, rhs); }
Int& operator/=(const int& rhs) {
DIV(val, rhs);
return *this;
}
Int operator/(const int& rhs) const { return qtt(val, rhs); }
Int operator-() const { return MOD - *this; }
};
} // namespace NT
using namespace NT;
constexpr 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() {
cin.tie(nullptr)->sync_with_stdio(false);
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--) {
cin >> n;
cout << "Connected: " << C[n] << '\n'
<< "Eulerian: " << E[n] << '\n'
<< "Bipartite: " << B[n] << "\n\n";
}
}
Exercises
- UOJ Goodbye Jihai D. New Year's Chase
- BZOJ 3864. Big Friends and Multiway Trees
- BZOJ 2863. Angry Leader
- Luogu P6295. Counting Labeled DAGs
- LOJ 6569. Cactus Counting
- LOJ 6570. Caterpillar Counting
- Luogu P5434. Counting Labeled Cactus-like Graphs
- Luogu P3343. [ZJOI2015] Fantasy Village after the Earthquake
- HDU 5279. YJC plays Minecraft
- Luogu P7364. Counting Labeled Bipartite Graphs
- Luogu P5827. Counting Vertex-Biconnected Graphs
- Luogu P5827. Counting Edge-Biconnected Graphs
- Luogu P6596. How Many of Them
- Luogu U152448. Counting Labeled Strongly Connected Graphs
- Project Euler 434. Rigid graphs
Riddell's Formula
The use of exp for EGFs above is sometimes called Riddell's formula for labeled graphs. The Euler transform of generating functions is also sometimes called Riddell's formula for unlabeled graphs. The latter first appeared in Euler's study of partition numbers; besides graph enumeration, it also appears in the complete-knapsack problem.
For a given sequence and its corresponding OGF , define the Euler transform of as:
Suppose the coefficients of are . Define the auxiliary array . Then:
Unlabeled Trees
Example: SPOJ PT07D, Let us count 1 2 3
Example: SPOJ PT07D, Let us count 1 2 3
Problem statement: count the number of trees on n vertices satisfying each of the following properties.
Rooted Trees
The labeled case was handled earlier. Now consider unlabeled rooted trees. Let their OGF be . Applying the Euler transform gives:
Extract the coefficients.
Unrooted Trees
Use inclusion-exclusion: subtract the rooted-tree configurations whose root is not a centroid from all rooted-tree configurations, and discuss the parity of .
When is odd:
There must be a subtree of size ; enumerating its size gives:
When is even:
When there are two centroids, the preceding process subtracts the tree only once, so we must subtract an additional term:
Example: Luogu P5900, Counting Unlabeled Unrooted Trees
Example: Luogu P5900, Counting Unlabeled Unrooted Trees
Problem statement: count the number of unlabeled unrooted trees on n vertices ().
For larger limits, the same approach applies: use a polynomial template after the Euler transform.
Unlabeled Simple Graphs
Example: SGU 282, Isomorphism
Example: SGU 282, Isomorphism
Problem statement: count the number of ways to color the edges of an unlabeled complete graph on n vertices with m colors.
When m = 2, the object being counted is precisely the unlabeled simple graph A000088. Consider the Pólya enumeration theorem:
In this problem, the permutation group is the permutation group on the edge set induced by the symmetric group on vertices. The brute-force enumeration has size , so it cannot solve the problem.
Classify permutations by their cycle structure. Each cycle structure corresponds to a partition. We use dfs() to generate the partitions, reducing the problem to finding, for each partition , the number of corresponding permutations and the number of cycles in each permutation class. The answer is:
Consider . Each partition corresponds to a cyclic arrangement, and the order of parts of equal size is irrelevant. Thus:
Here denotes the number of parts of size appearing in .
Now consider . The cycle on the set of vertices affected by has length . However, the problem colors edges, so we must also examine the edge permutation induced by the vertex permutation.
If the endpoints of an edge lie in the same cycle, let that cycle have size . Then the number of cycles generated on edges is exactly .
If the endpoints lie in two different cycles, let their sizes be and . Every orbit has length , so the number of cycles generated on edges is exactly .
Reference code
#include <iostream>
#include <vector>
using namespace std;
using LL = long long;
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)); }
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>
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>
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);
return *this;
}
Int operator+(const int& rhs) const { return sum(val, rhs); }
Int& operator-=(const int& rhs) {
DEC(val, rhs);
return *this;
}
Int operator-(const int& rhs) const { return dff(val, rhs); }
Int& operator*=(const int& rhs) {
MUL(val, rhs);
return *this;
}
Int operator*(const int& rhs) const { return pdt(val, rhs); }
Int& operator/=(const int& rhs) {
DIV(val, rhs);
return *this;
}
Int operator/(const int& rhs) const { return qtt(val, rhs); }
Int operator-() const { return MOD - *this; }
};
} // namespace NT
using namespace NT;
constexpr int N = int(5e1) + 9;
Int Fact[N];
vector<vector<int>> Partition;
vector<int> cur;
int n, m;
void gen(int n = ::n, int s = 1) {
if (!n) {
Partition.push_back(cur);
} else if (n >= s) {
cur.push_back(s);
gen(n - s, s);
cur.pop_back();
gen(n, s + 1);
}
}
Int w(const vector<int> P) {
Int z = Fact[n];
int c = 0, l = P.front();
for (auto p : P) {
z /= p;
if (p != l) {
z /= Fact[c];
l = p;
c = 1;
} else {
++c;
}
}
z /= Fact[c];
return z;
}
int gcd(int x, int y) { return y ? gcd(y, x % y) : x; }
int c(const vector<int> P) {
int z = 0;
for (int i = 0; i < P.size(); ++i) {
z += P[i] / 2;
for (int j = 0; j < i; ++j) z += gcd(P[i], P[j]);
}
return z;
}
int main() {
cin >> n >> m >> MOD;
Fact[0] = 1;
for (int i = 1; i <= n; ++i) Fact[i] = Fact[i - 1] * i;
gen();
Int res = 0;
for (auto P : Partition) {
res += w(P) * pow(m, c(P));
}
res /= Fact[n];
cout << res << endl;
}
Exercises
- CodeForces 438 E. The Child and Binary Tree
- Luogu P5448. [THUPC2018] Counting Good Graphs
- Luogu P5818. [JSOI2011] Counting Isomers
- Luogu P6597. Counting Alkenes
- Luogu P6598. Counting Alkanes
- Luogu P4128. [SHOI2006] Colored Graph
- Luogu P4727. [HNOI2009] Counting Graph Isomorphisms
- AtCoder Beginner Contest 222 H. Binary Tree
- AtCoder Beginner Contest 284 Ex. Count Unlabeled Graphs
- Luogu P4708. Drawing
- Luogu P7592. Counting Trees (2021 CoE-II E)
- Luogu P5206. [WC2019] Counting Trees
References and notes
- WC2015, Gu Yuzhou's training-camp exchange materials: Graphical Enumeration
- WC2019, Generating Functions, Polynomial Algorithms, and Graph Enumeration
- 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
Footnotes
-
Perhaps unlabeled binary trees are a counterexample: when the structure is simple, the corresponding permutation group is the identity group (Identity Group), so the labeled version can be obtained directly by multiplying by . ↩
-
PinkRabbit's blog notes that this sequence can also be optimized with the Chirp Z-Transform. ↩