Back to Graph Theory Series

Minimum Path Cover in DAGs

October 11, 2026 Wasil Zafar 25 min read

Cover every task in a dependency graph with as few disjoint chains as possible by turning a path problem into a matching problem.

Contents

  1. Paths from Pairings
  2. Problem and Variants
  3. Matching Reduction
  4. Worked Example
  5. Why the Formula Works
  6. Reconstruction
  7. Implementation
  8. Applications
  9. Pitfalls
  10. Complexity and Choice

From Separate Vertices to Connected Paths

Imagine beginning with every DAG vertex as a one-vertex path. There are $n$ paths. Whenever an edge $u\to v$ is chosen to place $v$ immediately after $u$, two paths merge and the count falls by one. The optimization question is therefore: how many compatible predecessor–successor links can we choose at once?

IntuitionThink of each vertex as a railcar. A directed edge says one car is allowed to follow another. Each car has one front coupler and one rear coupler, so it can have at most one predecessor and one successor. A maximum matching chooses the largest compatible set of couplings; the remaining connected trains are the minimum path cover.

The partial-order connection

DAGAcyclicity gives a consistent before–after structure.
Reachability“$u$ can reach $v$” defines a partial order.
DilworthFor reachability chains, minimum chain decomposition equals maximum antichain size. The edge-based path-cover variant below is closely related but does not silently add transitive edges.

The Problem—and Two Variants to Keep Separate

A vertex-disjoint path cover is a collection of directed paths in which every vertex appears exactly once. Paths of one vertex are allowed. The goal is to use as few paths as possible.

VariantWhen $u$ may precede $v$Bipartite edgesMeaning
Edge-based path coverThe original edge $u\to v$ existsUse original DAG edgesConsecutive path vertices are directly connected.
Reachability chain coverSome directed path connects $u$ to $v$Use the transitive closureVertices are comparable, even if other vertices lie between them.

This article’s default

Unless stated otherwise, “path cover” means the edge-based, vertex-disjoint version. Using transitive closure can produce a smaller number, but it answers the reachability-chain question instead.

Five-vertex DAG with a two-path coverThe DAG has edges A to B, A to C, B to D, C to D, C to E, and D to E. Highlighted edges form the path A, B, D, E while C forms a singleton second path. teal edges form one optimal path · the unmatched vertex C forms the second path ABCDE Path 1: A–B–D–E Path 2: C
One optimal cover is $A\to B\to D\to E$ plus the singleton path $C$. Other maximum matchings can produce different covers of the same minimum size.

Split Every Vertex, Then Match

Construct a bipartite graph with a left copy $v_L$ and a right copy $v_R$ of every DAG vertex. For each DAG edge $u\to v$, add the bipartite edge $u_L\to v_R$.

Left copy

Matching $u_L$ chooses at most one successor for $u$.

Right copy

Matching $v_R$ chooses at most one predecessor for $v$.

Acyclicity

The chosen predecessor–successor links cannot close into a directed cycle.

DAG Path-Cover Reduction
flowchart TD
    D[Copy every vertex left and right] --> E[Map each DAG edge u→v to uL→vR]
    E --> M[Find a maximum bipartite matching]
    M --> S[Matched pairs become successor links]
    S --> P[Begin at unmatched right copies and follow links]
Bipartite graph for the five-vertex DAGLeft and right copies of A through E are connected according to the original DAG edges. Matching edges A left to B right, B left to D right, and D left to E right are highlighted. A right and C right are unmatched, so A and C start the two reconstructed paths. Left copies: choose successorsRight copies: receive predecessors AₗBₗCₗDₗEₗ AᵣBᵣCᵣDᵣEᵣ unmatched → startunmatched → start maximum matching: A→B, B→D, D→E
The matching has size $3$. The unmatched right copies are $A_R$ and $C_R$, which become the starts of the two cover paths.

Worked Example: From Matching to Two Paths

The example DAG has five vertices and six edges:

$$E=\{A\to B,\ A\to C,\ B\to D,\ C\to D,\ C\to E,\ D\to E\}$$
Matched bipartite edgePath-cover interpretation
$A_L\to B_R$$B$ immediately follows $A$.
$B_L\to D_R$$D$ immediately follows $B$.
$D_L\to E_R$$E$ immediately follows $D$.

The matching size is $3$, so the cover size is:

$$|V|-|M_{\max}|=5-3=2$$

Right copies $A_R$ and $C_R$ are unmatched, so reconstruction starts at $A$ and $C$. Following matched successors produces $A\to B\to D\to E$ and $C$.

Check the flexibility of matching

Remove the edge $D\to E$. Does the minimum cover size increase?

Answer: no. A different size-3 matching—$A\to B$, $B\to D$, and $C\to E$—still gives two paths: $A\to B\to D$ and $C\to E$.

Why the Formula Is $n-|M_{\max}|$

The equality follows from two directions.

From a matching to a path cover

Each matched edge chooses one successor and one predecessor. Matching constraints prevent branching: no vertex receives two chosen predecessors, and no vertex chooses two successors. Because the original graph is acyclic, the selected links form disjoint paths, not cycles. Starting from $n$ singleton paths, each of the $|M|$ selected links merges two components, leaving $n-|M|$ paths.

From a path cover to a matching

Suppose a cover has $k$ paths. A path containing $r$ vertices uses exactly $r-1$ internal edges. Summed across all paths, the cover uses $n-k$ predecessor–successor links. Those links form a valid bipartite matching, because every vertex has at most one predecessor and one successor inside its path. Therefore a maximum matching is at least $n-k$, and minimizing $k$ is equivalent to maximizing the matching.

The invariant to remember

Every legal matched link saves exactly one path, and every saved path requires exactly one legal link. That one-for-one exchange is the entire reduction.

Reconstructing the Actual Paths

A matching algorithm usually returns pairLeft[u], the right vertex matched to left copy $u_L$, or “unmatched.” Convert it into two arrays:

ArrayDefinitionUse
successor[u]The vertex $v$ matched to $u_L$Follow the current path forward.
predecessor[v]The vertex $u$ whose left copy matched $v_R$Detect whether $v$ starts a path.
  1. For every matched pair $u_L\to v_R$, set successor[u] = v and predecessor[v] = u.
  2. Every vertex with no predecessor is a path start.
  3. From each start, repeatedly follow successor until none exists.

Common reconstruction mistake

Starts are found from unmatched right copies, not unmatched left copies. An unmatched left copy has no successor, so it marks a path end.

Implementation: Reuse the Matching Result

Run Hopcroft–Karp or a simpler augmenting-path matcher first. The following code turns its left-pair array into the actual cover.

def reconstruct_path_cover(pair_left):
    n = len(pair_left)
    successor = [-1] * n
    predecessor = [-1] * n

    for u, v in enumerate(pair_left):
        if v != -1:
            successor[u] = v
            predecessor[v] = u

    paths = []
    for start in range(n):
        if predecessor[start] != -1:
            continue
        path = []
        vertex = start
        while vertex != -1:
            path.append(vertex)
            vertex = successor[vertex]
        paths.append(path)
    return paths

pair_left = [1, 3, -1, 4, -1]
print(reconstruct_path_cover(pair_left))  # [[0, 1, 3, 4], [2]]
vector<vector<int>> reconstruct(const vector<int>& pairLeft) {
    int n = pairLeft.size();
    vector<int> successor(n, -1), predecessor(n, -1);
    for (int u = 0; u < n; ++u) {
        int v = pairLeft[u];
        if (v != -1) {
            successor[u] = v;
            predecessor[v] = u;
        }
    }
    vector<vector<int>> paths;
    for (int start = 0; start < n; ++start) {
        if (predecessor[start] != -1) continue;
        vector<int> path;
        for (int v = start; v != -1; v = successor[v])
            path.push_back(v);
        paths.push_back(path);
    }
    return paths;
}
static List<List<Integer>> reconstruct(int[] pairLeft) {
    int n = pairLeft.length;
    int[] successor = new int[n], predecessor = new int[n];
    Arrays.fill(successor, -1);
    Arrays.fill(predecessor, -1);
    for (int u = 0; u < n; u++) {
        int v = pairLeft[u];
        if (v != -1) {
            successor[u] = v;
            predecessor[v] = u;
        }
    }
    List<List<Integer>> paths = new ArrayList<>();
    for (int start = 0; start < n; start++) {
        if (predecessor[start] != -1) continue;
        List<Integer> path = new ArrayList<>();
        for (int v = start; v != -1; v = successor[v])
            path.add(v);
        paths.add(path);
    }
    return paths;
}

Implementation checklist

  • Verify the input is acyclic with a topological sort.
  • Create exactly one left and one right copy per original vertex.
  • Add a bipartite edge for each original DAG edge—unless reachability chains are explicitly intended.
  • Keep the matching pairs, not only the matching size.
  • Start reconstruction at vertices unmatched on the right.
  • Include isolated vertices as singleton paths.

Where Path Covers Are Useful

The reduction applies when a vertex must be used exactly once and an edge means one item may directly follow another.

Scheduling

Worker Sequences

If one worker may continue from task $u$ directly to compatible task $v$, each cover path is one worker’s route.

Data Lineage

Pipeline Chains

Compress a DAG into a small set of non-overlapping processing chains for display or operational ownership.

Compilation

Value Lifetimes

Compatibility edges can link values or operations that safely reuse a sequential resource.

Sequencing

Assembly Runs

Allowed transitions join jobs into the fewest disjoint production sequences.

Model the edge meaning carefully

A generic prerequisite edge does not automatically mean “the same worker can perform these consecutively.” The worker interpretation is valid only when edges encode allowed handoffs or sequencing, not merely dependency.

Pitfalls and Boundary Cases

CaseWhat happensResponse
Directed cycleA matching may select a cycle, so $n-|M|$ can even become zero.Reject or transform cyclic input; DAG acyclicity is essential.
Isolated vertexBoth copies remain unmatched.Return it as a singleton path.
Multiple maximum matchingsDifferent optimal path covers may be reconstructed.Add tie-breaking or weights only if the particular cover matters.
Transitive closure added silentlyThe result may shrink by linking merely reachable vertices.State that the goal is a reachability chain cover.
Paths may share verticesThe matching model no longer represents the problem.Use a formulation designed for overlap or flow multiplicity.

Complexity and Algorithm Choice

The split graph has $2n$ vertices and $m$ bipartite edges. Building it costs $O(n+m)$. Hopcroft–Karp finds a maximum matching in $O(m\sqrt{n})$ time and $O(n+m)$ memory; reconstruction adds $O(n)$. A topological-sort validation also costs $O(n+m)$.

SituationGood starting pointTradeoff
Small graph or simplest implementationDFS augmenting pathsEasy to code; worst case $O(nm)$.
Large sparse DAGHopcroft–KarpBetter asymptotic matching time.
Reachability-chain decompositionTransitive closure + matchingCorrect semantics, but closure can be dense and expensive.
Preferred or weighted transitionsWeighted matching or min-cost flowOptimizes which minimum cover is chosen.
General directed graphDifferent formulationThe DAG reduction no longer guarantees paths and the general problem is much harder.

Mental model to keep

Start with one path per vertex. A bipartite matching chooses the largest set of conflict-free successor links. Each link removes exactly one path, so the minimum number remaining is $n-|M_{\max}|$.