Prüfer Sequence

Note

This article is translated from e-maxx Prüfer Code. The original numbers vertices from 00, whereas this article uses the numbering convention more common in practice, starting from 11.

This article introduces the Prüfer sequence (Prüfer code), a method of representing a labeled tree by a unique integer sequence.

The Prüfer sequence can be used to prove Cayley's formula. We will also explain how to count the ways to add edges to a graph so that it becomes connected.

Note: We do not consider trees with 11 vertex.

Prüfer Sequence

Introduction

A Prüfer sequence represents a tree with nn labeled vertices by integers from [1,n][1,n], specifically n−2n-2 integers. It can also be understood as a bijection between the spanning trees of a complete graph and sequences, which is useful in common combinatorial counting problems.

Heinz Prüfer invented this sequence in 1918 to prove Cayley's formula.

Constructing a Prüfer Sequence from a Tree

The Prüfer sequence is constructed as follows: each time, choose and delete the leaf with the smallest label, and record the vertex to which it was connected in the sequence. After repeating this n−2n-2 times, only two vertices remain and the algorithm ends.

Clearly, using a heap gives complexity O(nlog⁡n)O(n\log n).

Implementation

C++

// Code excerpted from the original; vertices are numbered from 0
vector<vector<int>> adj;

vector<int> pruefer_code() {
  int n = adj.size();
  set<int> leafs;
  vector<int> degree(n);
  vector<bool> killed(n);
  for (int i = 0; i < n; i++) {
    degree[i] = adj[i].size();
    if (degree[i] == 1) leafs.insert(i);
  }

  vector<int> code(n - 2);
  for (int i = 0; i < n - 2; i++) {
    int leaf = *leafs.begin();
    leafs.erase(leafs.begin());
    killed[leaf] = true;
    int v;
    for (int u : adj[leaf])
      if (!killed[u]) v = u;
    code[i] = v;
    if (--degree[v] == 1) leafs.insert(v);
  }
  return code;
}

Python

# Vertices are numbered from 0
adj = [[]]


def pruefer_code():
    n = len(adj)
    leafs = set()
    degree = [0] * n
    killed = [False] * n
    for i in range(1, n):
        degree[i] = len(adj[i])
        if degree[i] == 1:
            leafs.intersection(i)
    code = [0] * (n - 2)
    for i in range(1, n - 2):
        leaf = leafs[0]
        leafs.pop()
        killed[leaf] = True
        for u in adj[leaf]:
            if killed[u] == False:
                v = u
        code[i] = v
        if degree[v] == 1:
            degree[v] = degree[v] - 1
            leafs.intersection(v)
    return code

For example, this is the construction process for the Prüfer sequence of a tree with 7 vertices:

Prüfer
Prüfer

The resulting sequence is 2,2,3,3,22,2,3,3,2.

There is also a linear-time construction algorithm.

Linear-Time Construction of a Prüfer Sequence

The essence of the linear construction is to maintain a pointer to the vertex we are about to delete. First observe that the number of leaves is non-increasing: deleting a leaf either leaves the total number of leaves unchanged or decreases it by 1.

We therefore consider the following process. Maintain a pointer pp. Initially, pp points to the leaf with the smallest label. At the same time, maintain the degree of every vertex so that we can determine whether deleting a vertex creates a new leaf. The operations are:

  1. Delete the vertex pointed to by pp, and check whether a new leaf is created.
  2. If a new leaf is created, suppose its label is xx and compare p,xp,x. If x>px>p, do nothing else; otherwise, immediately delete xx, check whether deleting xx creates another new leaf, and repeat step 22 until no new vertex is created or the new vertex has label >p>p.
  3. Increment the pointer pp until an undeleted leaf is encountered;

Correctness

Repeating the above operations n−2n-2 times completes the construction of the sequence. We now consider the correctness of the algorithm.

pp is the leaf with the smallest current label. If deleting pp creates no leaf, we can only search for the next leaf; if it creates a leaf xx:

  • If x>px>p, scanning forward from pp will reach it anyway, so do nothing;
  • If $x> adj; vector<int> parent;

void dfs(int v) { for (int u : adj[v]) { if (u != parent[v]) parent[u] = v, dfs(u); } }

vector<int> pruefer_code() { int n = adj.size(); parent.resize(n), parent[n - 1] = -1; dfs(n - 1);

int ptr = -1; vector<int> degree(n); for (int i = 0; i < n; i++) { degree[i] = adj[i].size(); if (degree[i] == 1 && ptr == -1) ptr = i; }

vector<int> code(n - 2); int leaf = ptr; for (int i = 0; i < n - 2; i++) { int next = parent[leaf]; code[i] = next; if (--degree[next] == 1 && next < ptr) { leaf = next; } else { ptr++; while (degree[ptr] != 1) ptr++; leaf = ptr; } } return code; }


**Python**

```python
# Likewise, starting from 0
adj = [[]]
parent = [0] * n

def dfs(v):
    for u in adj[v]:
        if u != parent[v]:
            parent[u] = v
            dfs(u)

def pruefer_code():
    n = len(adj)
    parent[n - 1] = -1
    dfs(n - 1)

    ptr = -1
    degree = [0] * n
    for i in range(0, n):
        degree[i] = len(adj[i])
        if degree[i] == 1 and ptr == -1:
            ptr = i

    code = [0] * (n - 2)
    leaf = ptr
    for i in range(0, n - 2):
        next = parent[leaf]
        code[i] = next
        if degree[next] == 1 and next < ptr:
            degree[next] = degree[next] - 1
            leaf = next
        else:
            ptr = ptr + 1
            while degree[ptr] != 1:
                ptr = ptr + 1
            leaf = ptr
    return code

Properties of the Prüfer Sequence

  1. After constructing the Prüfer sequence, two vertices remain in the original tree, one of which is necessarily the vertex with the largest label, nn.
  2. Each vertex appears in the sequence a number of times equal to its degree minus 11. (Vertices that do not appear are leaves.)

Reconstructing a Tree from a Prüfer Sequence

The reconstruction method is similar. From the properties of the Prüfer sequence, we can obtain the degree of every vertex in the original tree. We can then find the leaf with the smallest label; this leaf must be connected to the vertex corresponding to the first number in the Prüfer sequence. We then decrease the degrees of these two vertices by one.

By this point you may already know how to proceed. Each time, choose the smallest-labeled vertex of degree 11, connect it to the vertex currently selected from the Prüfer sequence, and decrease the degrees of both vertices. At the end, two vertices of degree 11 remain, one of which is vertex nn; connect them. Maintaining this process with a heap, and adding a vertex to the heap whenever its degree decreases to 11, gives complexity O(nlog⁡n)O(n\log n).

Implementation
// Code excerpted from the original
vector<pair<int, int>> pruefer_decode(vector<int> const& code) {
  int n = code.size() + 2;
  vector<int> degree(n, 1);
  for (int i : code) degree[i]++;

  set<int> leaves;
  for (int i = 0; i < n; i++)
    if (degree[i] == 1) leaves.insert(i);

  vector<pair<int, int>> edges;
  for (int v : code) {
    int leaf = *leaves.begin();
    leaves.erase(leaves.begin());

    edges.emplace_back(leaf, v);
    if (--degree[v] == 1) leaves.insert(v);
  }
  edges.emplace_back(*leaves.begin(), n - 1);
  return edges;
}

Linear-Time Tree Reconstruction

Use the same method as for the linear construction of a Prüfer sequence. When deleting degree, a new leaf may be created; compare this leaf with the pointer pp and process it first if its label is smaller.

Implementation

// Code excerpted from the original
vector<pair<int, int>> pruefer_decode(vector<int> const& code) {
  int n = code.size() + 2;
  vector<int> degree(n, 1);
  for (int i : code) degree[i]++;

  int ptr = 0;
  while (degree[ptr] != 1) ptr++;
  int leaf = ptr;

  vector<pair<int, int>> edges;
  for (int v : code) {
    edges.emplace_back(leaf, v);
    if (--degree[v] == 1 && v < ptr) {
      leaf = v;
    } else {
      ptr++;
      while (degree[ptr] != 1) ptr++;
      leaf = ptr;
    }
  }
  edges.emplace_back(leaf, n - 1);
  return edges;
}

These processes make it clear that the Prüfer sequence establishes a bijection between labeled unrooted trees and sequences.

Cayley Formula (Cayley's formula)

The complete graph KnK_n has nn−2n^{n-2} spanning trees.

How can we prove this? There are many methods, but a Prüfer-sequence proof is very simple. Every integer sequence of length n−2n-2 with value range [1,n][1,n] corresponds bijectively to a spanning tree through the Prüfer sequence, so the number of possibilities is nn−2n^{n-2}.

Number of Ways to Make a Graph Connected

The Prüfer sequence may be more powerful than you expect. It yields formulas more general than Cayley's formula. Consider the following problem:

An undirected labeled graph with nn vertices, mm edges, and kk connected components is given. We want to add k−1k-1 edges so that the entire graph becomes connected. Count the number of ways.

Proof

Let sis_i denote the number of vertices in the ii-th connected component. We consider constructing a Prüfer sequence on the kk connected components. Since there are many ways to connect two components, this is not an ordinary Prüfer sequence. Let did_i be the degree of the ii-th connected component. Since the sum of the degrees is twice the number of edges, ∑i=1kdi=2k−2\sum_{i=1}^kd_i=2k-2. For a fixed sequence dd, the number of ways to construct the Prüfer sequence is

(k−2d1−1,d2−1,⋯ ,dk−1)=(k−2)!(d1−1)!(d2−1)!⋯(dk−1)!\binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1}=\frac{(k-2)!}{(d_1-1)!(d_2-1)!\cdots(d_k-1)!}

For the ii-th connected component, there are sidi{s_i}^{d_i} ways to choose its connections. Therefore, for a fixed sequence dd, the number of ways to make the graph connected is

(k−2d1−1,d2−1,⋯ ,dk−1)⋅∏i=1ksidi\binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1}\cdot \prod_{i=1}^k{s_i}^{d_i}

We now enumerate the sequences dd, giving

∑di≥1,∑i=1kdi=2k−2(k−2d1−1,d2−1,⋯ ,dk−1)⋅∏i=1ksidi\sum_{d_i\ge 1,\sum_{i=1}^kd_i=2k-2}\binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1}\cdot \prod_{i=1}^k{s_i}^{d_i}

This is not a particularly pleasant expression. But do not panic! We have the multinomial theorem:

(x1+⋯+xm)p=∑ci≥0, ∑i=1mci=p(pc1,c2,⋯ ,cm)⋅∏i=1mxici(x_1 + \dots + x_m)^p = \sum_{\substack{c_i \ge 0 ,\ \sum_{i=1}^m c_i = p}} \binom{p}{c_1, c_2, \cdots ,c_m}\cdot \prod_{i=1}^m{x_i}^{c_i}

Now make the substitution ei=di−1e_i=d_i-1 in the original expression. Clearly, ∑i=1kei=k−2\sum_{i=1}^ke_i=k-2, so it becomes

∑ei≥0,∑i=1kei=k−2(k−2e1,e2,⋯ ,ek)⋅∏i=1ksiei+1\sum_{e_i\ge 0,\sum_{i=1}^ke_i=k-2}\binom{k-2}{e_1,e_2,\cdots,e_k}\cdot \prod_{i=1}^k{s_i}^{e_i+1}

Simplifying gives

(s1+s2+⋯+sk)k−2⋅∏i=1ksi(s_1+s_2+\cdots+s_k)^{k-2}\cdot \prod_{i=1}^ks_i

That is

nk−2⋅∏i=1ksin^{k-2}\cdot\prod_{i=1}^ks_i

which is the answer.

Exercises

This content comes from OI Wiki: Prüfer Sequence. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.