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)};
    }
};

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];
    }
};

A naive hash is not safe

h = h * B + child without 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-height x[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.

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 treedecode 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.

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};
}