The hard part of a graph problem is usually noticing it is a graph problem, and choosing the right vertices. This page is a lookup table from problem phrasing to construction.

Recognising the graph

PhrasingVerticesEdges
”minimum number of operations to transform X into Y”statesone operation
”can we reach this configuration”configurationsone move
”who must come before whom”itemsprecedence
”these two cannot both be chosen”choicesconflict — think 2-SAT or bipartite
”assign each A to a B”A’s and B’scompatibility — matching
”minimum cost to separate / block”as givenmin cut
”maximum number of disjoint routes”as givenmax flow
”connect everything cheaply”as givenMST
”cheapest route”as givenshortest path

The state-space transformations

Layered graphs — state that travels with you

When movement depends on something you carry, make the vertex a pair.

Carried stateVertex
Keys collected
Fuel remaining
Moves used mod
“Have I used my one free teleport?”
Which of discounts remain
Current speed / direction

The graph grows by a factor equal to the number of states; the algorithm is unchanged.

Node splitting — constraints on vertices

Split into and put the constraint on the internal edge:

  • vertex capacity → capacity on the internal edge;
  • vertex cost → cost on the internal edge;
  • vertex-disjoint paths → internal capacity 1;
  • “visit at most special cities” → count on the internal edge.

Edge subdivision — costs that depend on usage

Replace an edge by several parallel edges with increasing cost. Because min-cost flow uses the cheapest first, this exactly models a convex piecewise-linear cost.

Reversal — “how many edges must I flip”

Add each edge forward with weight 0 and backward with weight 1, then run 0-1 BFS.

Complement graphs

When the graph is dense but its complement is sparse, run BFS on the complement in total by keeping the unvisited vertices in a set and erasing as you go. This turns “BFS on a graph with edges” into something feasible.

set<int> unvisited;                 // all vertices initially
while (!q.empty()) {
    int u = q.front(); q.pop();
    vector<int> toVisit;
    for (int v : unvisited)
        if (!adjSet[u].count(v)) toVisit.push_back(v);   // edge in the complement
    for (int v : toVisit) { unvisited.erase(v); q.push(v); }
}

Reductions worth memorising

ProblemReduces to
Minimum path cover of a DAGbipartite matching
Maximum independent set, bipartite − max matching
Minimum vertex cover, bipartitemax matching (König)
Maximum antichain in a posetDilworth → matching
Project selection / maximum closuremin cut
Binary labelling with pairwise penaltiesmin cut
”Every road at least once”Chinese postman
disjoint minimum-cost pathsmin-cost flow of value
Difference constraints Bellman-Ford
Boolean constraints, two literals per clause2-SAT
Maximise the minimum edge on a pathMST path maximum
Reachability with cyclescondense, then DAG DP
Distance queries between many pairs on a treeLCA

Sanity checks before coding

  1. Directed or undirected? Getting this wrong invalidates everything downstream.
  2. Are there self-loops or parallel edges? They break naive parent-skipping and DSU-based cycle checks.
  3. Is the graph connected? Many algorithms need a loop over all start vertices.
  4. Are weights non-negative? If not, Dijkstra is silently wrong.
  5. Does fit the algorithm? needs ; needs .
  6. Is the answer “impossible”? Decide the sentinel value before writing the loop.

See also: Complexity Cheatsheet · Representations · Problem Pattern Recognition