N or P
Contents Glossary
37

Is this in the cheap category — a checklist

Before you size an engineering effort, run the six tests. Half the time the problem is already solved.

Six structural tests determine whether a problem is in the cheap category. Greedy with exchange argument — does picking the best local choice provably lead to optimal? (Many sorting, scheduling, allocation problems.) Dynamic programming — is there a small set of subproblems that combine into the answer? (Most counting, optimization, alignment problems.) Network flow — does the problem reduce to "maximum flow through a network with capacities"? (Many matching, allocation, partition problems.) Two-piece constraints — are all the constraints expressible as "if X then Y" with exactly two variables? (Many configuration, scheduling problems.) Linear programming — are constraints and objective linear with continuous variables? (Most operational planning.) Linear algebra — does the problem reduce to matrix factorization, eigenvalues, or linear systems? (Many ranking, recommendation, signal processing problems.) If any test passes, you have a settled problem and a library. Run the checklist in the meeting.

  1. 01
    The six tests as an enum the codebase carries. Each branch points at the right crate. Wire this into your planning template and the procurement question becomes a constructor argument, not a debate.
    enum CheapTest {
        GreedyWithExchange,   // sorting, scheduling, MST
        DynamicProgramming,   // counting, alignment, DAGs
        NetworkFlow,          // matching, allocation, partition
        TwoPieceConstraint,   // 2-SAT via SCC
        LinearProgramming,    // continuous vars + linear constraints
        LinearAlgebra,        // matrices, eigenvalues, factorization
    }
    
    fn crate_for(test: CheapTest) -> &'static str {
        match test {
            CheapTest::GreedyWithExchange => "std::slice::sort_unstable + a loop",
            CheapTest::DynamicProgramming => "hand-rolled table",
            CheapTest::NetworkFlow        => "petgraph::algo::ford_fulkerson",
            CheapTest::TwoPieceConstraint => "petgraph::algo::tarjan_scc on implication graph",
            CheapTest::LinearProgramming  => "good_lp with the HiGHS backend",
            CheapTest::LinearAlgebra      => "nalgebra or faer",
        }
    }
  2. 02
    enum is Rust's tagged union — a single type whose values can be one of several named variants. Page 9's Option and Result were enums with data-carrying variants (Some(T) holding a value, Err(E) holding an error). This page's CheapTest is the simpler form — every variant is a unit variant, carrying no payload. Each variant is just a name that the type system treats as distinct. Compared to a plain integer or string constant, an enum gets you the exhaustiveness check (page 9 again): the compiler refuses to compile a match that doesn't cover every variant, so adding a seventh test means the compiler points at every match in the codebase that needs updating. That refactor-safety is the whole reason to reach for an enum instead of a string.
    // Unit variants — no payload, just a name.
    enum Direction { North, South, East, West }
    
    // Data-carrying variants — each variant holds different data.
    enum Shape {
        Circle(f64),               // tuple-style: one f64 (radius)
        Rect(f64, f64),            // tuple-style: width, height
        Point,                      // unit variant — no data
        Annotated { name: String, kind: u8 },   // struct-style: named fields
    }
    
    // Build a variant by writing its full path:
    let d = Direction::East;
    let s = Shape::Circle(3.0);
    
    
    // Why an enum and not a string constant or integer?
    //
    //   1. Exhaustiveness — match arms get checked at compile time.
    //      Add a variant → compiler points at every match that needs updating.
    //
    //   2. Type safety — you can't accidentally pass a Shape where a
    //      Direction is expected; the types are distinct.
    //
    //   3. Pattern matching — destructure data-carrying variants in one step.
    //      Strings can't do that.
    
    
    // Page 37 uses pure unit variants — the checklist categories.
    // The match in `crate_for` covers all six, and the compiler will
    // refuse to compile if a seventh category gets added without a
    // corresponding arm.
    
    
    // One last note — enums under the hood:
    //
    //   Rust stores an enum as a discriminant (small integer tag) plus
    //   enough space for the largest variant's data.  Unit-only enums
    //   compile down to just the tag — `size_of::<CheapTest>() == 1` byte.
    //   Data-carrying enums are bigger.  Either way, dispatch on the
    //   variant is a single comparison, not a function call or a hash.
Cormen, Leiserson, Rivest, Stein (2009) Introduction to Algorithms. Source →
In plain terms

The trap that motivated this book is reaching for hard-problem tools — SAT solvers, ILP modeling, heuristic search — on problems that are actually in the cheap category. The counter is a checklist run at the planning stage. Six structural tests cover most of the cheap category.

Test one: greedy with exchange argument. Can the problem be solved by sorting and picking in order? Interval scheduling (maximize non-overlapping appointments), fractional knapsack, Huffman coding, minimum spanning tree, and dozens of others all have this shape. If yes, the work is a sort plus a loop. Days.

Test two: dynamic programming. Is there a small set of subproblems whose answers combine into the answer? Most counting problems, most optimization on sequences or DAGs, most alignment problems. Edit distance, shortest path on DAGs, matrix chain multiplication, knapsack DP all fit. If yes, the work is a table and a recurrence. Days.

Test three: network flow. Can the problem be expressed as "maximize the amount that can flow from source to sink through a capacitated network"? Many matching, allocation, partition, and assignment problems reduce to flow. Bipartite matching, project selection, image segmentation, baseball elimination — all flow. If yes, the work is to define the network and call the flow algorithm. Days.

Test four: two-piece constraints. Are all the rules expressible as "if X then Y" with exactly two variables each? Many configuration and scheduling problems have this shape. If yes, the work is to build the graph" data-term="implication graph">implication graph and run strongly connected components. Days.

Test five: linear programming. Are the constraints and objective all linear, and do the variables only need to be continuous? Most operational planning, blending, transportation, portfolio allocation. If yes, the work is to model in lp" data-term="good_lp">good_lp and call HiGHS. Days.

Test six: linear algebra. Does the problem reduce to matrix factorization, eigenvalues, or solving a linear system? PageRank, recommendation systems via matrix factorization, principal component analysis, spectral clustering. If yes, the work is to set up the matrix and call nalgebra or faer. Days.

If any test passes, you have a settled problem. Reach for the library and ship.

If no test passes, you may be in the expensive category — see page 38 for the lookalikes that fool teams and page 39 for the common mistakes.

← 36 When everything else fails — heuristics
37/39
38 Eight twins — one cheap, one expensive →