Two trees are isomorphic if some bijection on vertices preserves adjacency: . Graph isomorphism in general has no known polynomial algorithm (quasipolynomial, Babai 2016) and is not known to be in P or NP-complete; in practice tools like nauty use refinement plus backtracking. For trees specifically it drops to : rooted trees are compared by canonical forms built bottom-up (AHU); unrooted trees are rooted at their centre(s) first. Labelled trees on vertices are in bijection with sequences of length (Prüfer codes), which proves Cayley’s and turns degree-constrained tree counting into sequence counting.
AHU canonical ids
Give every rooted subtree an integer id: a vertex’s id is determined by the sorted list of its children’s ids (looked up in a dictionary shared by all trees being compared). Two rooted trees are isomorphic iff their roots get the same id. .
struct TreeCanon {
map<vector<int>, int> ids; // shared across all trees compared
int rooted(const vector<vector<int>>& adj, int root, vector<int>* all = nullptr) {
int n = sz(adj); vector<int> par(n, -1), order{root}, id(n);
for (int i = 0; i < sz(order); i++) for (int v : adj[order[i]]) if (v != par[order[i]]) par[v] = order[i], order.push_back(v);
for (int i = n - 1; i >= 0; i--) {
int u = order[i]; vector<int> ch;
for (int v : adj[u]) if (v != par[u]) ch.push_back(id[v]);
sort(all(ch));
auto it = ids.find(ch);
id[u] = it != ids.end() ? it->second : ids[ch] = sz(ids);
}
if (all) *all = id; // ids of every rooted subtree
return id[root];
}
pair<int, int> unrooted(const vector<vector<int>>& adj) { // canonical: sorted ids at the centre(s)
auto c = treeCenters(adj);
int a = rooted(adj, c[0]), b = sz(c) > 1 ? rooted(adj, c[1]) : a;
return {min(a, b), max(a, b)};
}
};Proof: AHU ids characterise rooted isomorphism; centres for unrooted trees
By induction on height: two rooted trees are isomorphic iff there is a bijection between the children of the roots matching isomorphic child subtrees, iff the multisets of child classes are equal, iff the sorted lists of child ids are equal (ids are in bijection with classes by the inductive hypothesis), iff the roots get the same id. Unrooted: an isomorphism maps centres to centres (eccentricity is preserved), so iff rooted at some centre is isomorphic to rooted at some centre; when a tree has two centres both rootings are taken, and comparing the sorted pairs covers every case (a tree with one centre gives the pair , which can only equal another one-centre pair). Time: each vertex’s children are sorted, in total, plus dictionary lookups whose key lengths sum to .
Hashing with a guarantee
For one-off comparisons or many trees, hash instead: with a random per height , set and . Isomorphic rooted trees get equal values, and non-isomorphic ones collide with probability ; use .
struct TreeHash { // P(u) = prod over children (x[height(u)] + P(c)) mod 2^61-1
static constexpr ull MOD = (1ULL << 61) - 1;
static ull mul(ull a, ull b) { __uint128_t c = (__uint128_t)a * b; ull r = (ull)(c >> 61) + (ull)(c & MOD); return r >= MOD ? r - MOD : r; }
vector<ull> x; mt19937_64 rng{chrono::steady_clock::now().time_since_epoch().count()};
ull rooted(const vector<vector<int>>& adj, int root, vector<ull>* all = nullptr) {
int n = sz(adj); vector<int> par(n, -1), order{root}, h(n); vector<ull> P(n, 1);
for (int i = 0; i < sz(order); i++) for (int v : adj[order[i]]) if (v != par[order[i]]) par[v] = order[i], order.push_back(v);
for (int i = n - 1; i > 0; i--) h[par[order[i]]] = max(h[par[order[i]]], h[order[i]] + 1);
while (sz(x) <= n) x.push_back(rng() % MOD);
for (int i = n - 1; i >= 0; i--) {
int u = order[i];
for (int v : adj[u]) if (v != par[u]) { ull t = x[h[u]] + P[v]; if (t >= MOD) t -= MOD; P[u] = mul(P[u], t); }
}
if (all) *all = P;
return P[root];
}
};Proof: the polynomial hash is exact up to Schwartz–Zippel
View as a polynomial in the variables . A child of has , so does not involve , and is a monic polynomial in with roots over the ring of the other variables. Polynomial rings over a field are unique factorisation domains, so determines its height (the variable it is monic in; leaves are the constant 1) and the multiset . By induction, equal polynomials mean isomorphic rooted trees, and isomorphic trees obviously give equal polynomials. has total degree , so two different polynomials agree at a uniformly random point modulo the prime with probability (Schwartz–Zippel); with and up to comparisons the failure probability is about .
A naive hash is not safe
h = h * B + childwithout sorting is order-dependent and wrong (two orderings of the same multiset of children collide to different values when they should be equal). Summing child hashes without the per-heightx[h]shift scramble collides trivially: children hashing to collide with any . Both the sort-then-combine (AHU) and the random-shift-then-multiply (hashing above) structure are required, not optional simplifications.
Uses: count distinct subtree shapes (collect all ids), group equal subtrees, find all repeated subtrees (group by subtree hash), check a tree for symmetry, memoise DP over tree shapes, compare unrooted trees (hash at the centres), count non-isomorphic trees satisfying extra constraints via DP over canonical forms (distinct from the unconstrained enumeration below, which has a closed recurrence).
Automorphisms
The number of automorphisms of a rooted tree is times the product over children, i.e. at each vertex, children of the same class may be permuted. For an unrooted tree with one centre root there; with two centres , multiply the two rooted counts and double if the halves are isomorphic.
Proof: automorphism count
An automorphism of a rooted tree fixes the root, so it permutes the root’s children, mapping each child subtree to an isomorphic one. Conversely, choosing for each class of isomorphic children a permutation of that class ( ways), an isomorphism between the swapped subtrees, and an automorphism inside each child subtree, gives an automorphism; the isomorphisms between two isomorphic subtrees are in bijection with the automorphisms of one of them. Induction gives the product formula. For two centres, every automorphism maps the central edge to itself, either fixing or swapping its ends.
Prüfer codes
Encode: times, delete the smallest-labelled leaf and record its neighbour. Decode: degree of = 1 + occurrences in the code; repeatedly join the smallest current leaf (degree 1) to the next code entry. Both run in with a pointer that only moves forward.
vector<int> pruferEncode(const vector<vector<int>>& adj) { // n >= 2
int n = sz(adj); vector<int> par(n, -1), order{n - 1}, deg(n), code;
for (int i = 0; i < sz(order); i++) for (int v : adj[order[i]]) if (v != par[order[i]]) par[v] = order[i], order.push_back(v); // root n-1
for (int v = 0; v < n; v++) deg[v] = sz(adj[v]);
int ptr = 0; while (deg[ptr] != 1) ptr++;
for (int leaf = ptr, i = 0; i < n - 2; i++) {
int nx = par[leaf]; code.push_back(nx);
if (--deg[nx] == 1 && nx < ptr) leaf = nx; // the neighbour became the smallest leaf
else { while (deg[++ptr] != 1); leaf = ptr; }
}
return code;
}
vector<pair<int, int>> pruferDecode(int n, const vector<int>& code) {
vector<int> deg(n, 1); for (int x : code) deg[x]++;
int ptr = 0; while (deg[ptr] != 1) ptr++;
int leaf = ptr; vector<pair<int, int>> edges;
for (int x : code) {
edges.push_back({leaf, x});
if (--deg[x] == 1 && x < ptr) leaf = x;
else { while (deg[++ptr] != 1); leaf = ptr; }
}
edges.push_back({leaf, n - 1});
return edges;
}| Count (labelled vertices ) | Value |
|---|---|
| trees (Cayley) | |
| trees with degrees () | |
| trees in which is a leaf | |
| rooted trees / rooted forests | / |
| forests of trees with in different trees | |
| spanning trees of | |
| uniform random labelled tree | decode uniform random labels |
Decoding uniform random labels samples a labelled tree uniformly in — the standard way to generate tree test cases for stress testing, and far better than “add random edges, reject if a cycle forms,” which is both biased and slow. For spanning trees of an arbitrary (non-complete) graph, sample uniformly with Wilson’s algorithm (loop-erased random walks) instead, or count them exactly with the Matrix-Tree theorem — Prüfer codes only handle the complete-graph case, bijectively and constructively.
Proof: Prüfer bijection and the counting table
Bijection. A vertex is deleted only when it is a leaf, so it is recorded exactly times (each recorded occurrence is the deletion of one neighbour, and its last remaining neighbour is never recorded for it unless it survives). Hence from the code we know every degree, and at each step the deleted leaf is the smallest vertex whose remaining recorded count is 0 among those not yet deleted: the decoder reproduces exactly the encoder’s choices, so encoding is injective. Any sequence decodes to a tree (each step joins a current leaf to a vertex that stays in the graph; edges without a cycle since each leaf is removed after use), and re-encoding returns the sequence, so it is a bijection between trees and . Pointer: a new leaf smaller than
ptrmust be the vertex just recorded, and it is then the minimum; otherwise the next leaf is found by advancingptr, which moves times overall.Table. Degrees: codes with appearing times are counted by the multinomial. Leaf : never appears, . Rooted trees: choose a root, . Rooted forests: add a vertex joined to all roots, a bijection with trees on vertices. Forests with roots : delete the smallest non-root leaf times, recording its neighbour; the result is a sequence whose first entries are arbitrary and whose last entry is a root (after the last deletion only roots remain), and the same decoding argument makes this a bijection: . : the Laplacian has eigenvalues , ( times), ( times) and , so by the matrix-tree theorem the count is . Uniform sampling: the decoder is a bijection from uniform sequences.
Counting unlabelled trees
Rooted unlabelled trees (1, 1, 2, 4, 9, 20, 48, 115, …) satisfy the Euler-transform recurrence , and unrooted trees follow from Otter: (1, 1, 1, 2, 3, 6, 11, 23, 47, 106, …). modulo a prime .
pair<vector<mint>, vector<mint>> countUnlabelledTrees(int N) { // r[1..N] rooted, t[1..N] unrooted
vector<mint> r(N + 1), t(N + 1), s(N + 1); if (N >= 1) r[1] = 1;
for (int d = 1; d <= N; d++) { // s[k] = sum_{d | k} d r_d, filled once r_d is known
if (d >= 2) { mint acc = 0; for (int k = 1; k < d; k++) acc += s[k] * r[d - k]; r[d] = acc / mint(d - 1); }
for (int k = d; k <= N; k += d) s[k] += mint(d) * r[d];
}
for (int n = 1; n <= N; n++) {
mint pairs = 0; for (int i = 1; i < n; i++) pairs += r[i] * r[n - i];
if (n % 2 == 0) pairs -= r[n / 2];
t[n] = r[n] - pairs / mint(2);
}
return {r, t};
}Proof: Euler transform and Otter's formula
Rooted: a rooted tree on vertices is a root with a multiset of rooted subtrees of total size , so . Taking the logarithmic derivative, with ; comparing coefficients of in gives , the recurrence (the loop computes from , all known). Otter (dissimilarity theorem): for any tree, , where and count vertex and edge orbits under its automorphisms and iff some edge has its ends swapped by an automorphism. Automorphisms fix the centre (or the central edge as a set), so mapping each vertex orbit to the orbit of the edge leading towards the centre is well defined. With one centre it is a bijection between vertex orbits other than and all edge orbits, and (a swapped edge’s midpoint would be a fixed centre point between two vertices): . With central edge that is swapped (), is one vertex orbit and the central edge an extra edge orbit outside the bijection: . If it is not swapped (), ‘s orbit is the only one left out (map to the central edge): . Summing over all unlabelled trees on vertices: (a tree with a chosen vertex orbit is a rooted tree), = number of trees with a distinguished edge orbit = unordered pairs of rooted trees of total size = , and (two equal halves). So .