This is a blog 4 of a series of blogs about algorithmic challenges I came across when creating tgen.
In this blog we will tackle:
- Generate a uniformly random tree on vertices given some preset edges, in time.
- Generate a skewed tree on vertices in time.
1. Uniform random tree with preset edges
Sometimes we want to force some edges to appear in the tree, for example because the test needs a specific path or star substructure. The preset edges may span several connected components; we need to connect those components into a single tree.
The key tool is the Prüfer sequence. Every labeled tree on vertices corresponds to a unique sequence of length with entries in , and the degree of vertex in the tree is one plus its frequency in the sequence. A uniform random Prüfer sequence therefore gives a uniform random labeled tree.
When there are preset edges, the components play the role of super-vertices: we want to connect components into a tree, which requires exactly new edges. The new tree on super-vertices is generated via a random Prüfer sequence of length . Larger components need to be more likely to be chosen, because they have more vertices that can be edge endpoints, so we sample each Prüfer entry with probability proportional to the component size using the alias method. For each new edge, a concrete vertex in each of its two components is then picked uniformly at random.
// c = number of components, comp_size[i] = size of component i
// component_ids[i] = list of vertex ids in component i
// many_by_distribution uses the alias method; returns i with probability proportional to comp_size[i]
std::vector<int> prufer = many_by_distribution(c - 2, comp_size);
for (auto [u, v] : edges_from_prufer(prufer))
new_edges.emplace_back(pick(component_ids[u]), pick(component_ids[v]));
Algorithm 1: connecting components with a weighted Prüfer sequence.
Theorem 1: Algorithm 1 returns each labeled tree on that contains the given preset edges with the same probability.
Proof: Let the preset edges form components of sizes , and write . Any labeled tree containing those edges contracts to a unique tree on the components. If has degrees , component appears times in the Prüfer sequence of .
Algorithm 1 samples each of the Prüfer entries independently with probability for component . Tree has a unique Prüfer sequence , so
Each super-edge is then replaced by an edge between uniformly random vertices of those components. A fixed choice of original endpoints has probability
Multiplying, the probability of any specific labeled tree is
which does not depend on the tree. (If there are no preset edges, then and , so this is the uniform distribution over all labeled trees.)
Sampling and decoding the Prüfer sequence are , and building the components is , so the whole procedure runs in time.
2. Skewed tree
A uniformly random labeled tree has expected height (see this comment). If the parent of is next(0, i-1), we get a classical random recursive tree, and the diameter is typically . To force a star, we want that parent to be close to ; to force a path (a skewed tree, with endpoints and ), we want it close to .
That bias is exactly wnext. For , wnext(i, e) is the maximum of independent samples of next(0, i-1) (so it is biased toward ). For , it is the minimum of samples (biased toward ).
int wnext(int i, int e) {
int j = next(0, i - 1);
if (e >= 0) {
for (int t = 0; t < e; t++)
j = std::max(j, next(0, i - 1));
} else {
for (int t = 0; t < std::abs(e); t++)
j = std::min(j, next(0, i - 1));
}
return j;
}
std::vector<std::pair<int, int>> gen_skewed(int n, int e) {
std::vector<std::pair<int, int>> edges;
for (int i = 1; i < n; i++)
edges.push_back({wnext(i, e), i});
return edges;
}
Algorithm 2: skewed tree by
wnext.
If is , this is the uniform recursive tree. If is negative and large in absolute value, this is likely a star centered at . If is large, this is likely a path with endpoints and .

Algorithm 2 with and (star-like).

Algorithm 2 with and (path-like).
In particular, as grows, wnext(i, e) concentrates on , so vertex connects to with probability tending to . For , it concentrates on .
The loop above runs in time, which is too slow for large . The same distribution can be sampled in by inverse transform:
int wnext(int i, int e) {
double r = next(0.0, 1.0);
double x;
if (e >= 0)
x = std::pow(r, 1.0 / (e + 1));
else
x = 1.0 - std::pow(r, 1.0 / (std::abs(e) + 1));
return (int)(x * i); // in [0, i)
}
Algorithm 3:
wnextin time.
Theorem 2: Algorithm 3 has the same distribution as wnext in Algorithm 2. Using it, Algorithm 2 runs in time.
Proof: Let , and let be uniform in . Write for independent uniform samples from .
If , Algorithm 3 returns , and
which is also .
If , Algorithm 3 returns , and
which is also .
Each call is , and Algorithm 2 makes calls.
References
Prüfer sequence (Wikipedia): https://en.wikipedia.org/wiki/Pr%C3%BCfer_sequence
Alias method (miniblog 3): https://codeforces.com/blog/entry/156111
Random recursive tree (Wikipedia): https://en.wikipedia.org/wiki/Random_recursive_tree
Height of a uniform random labeled tree: https://codeforces.com/blog/entry/95463#comment-845014
testlib wnext tree generator: https://codeforces.com/blog/entry/18291