N or P
Contents Glossary
04

Hard, harder, hardest — the labels

The vocabulary that separates "expensive but solvable" from "buy the SaaS instead."

Four labels show up in engineering conversations about complexity. P is the cheap category — fast algorithms exist. NP-complete is the canonical hard category — every problem in it is as hard as every other, and no fast algorithm is known despite enormous effort. NP-hard is at least that hard, and may be even harder. Co-NP is the mirror — problems where catching a violation is easy but proving a clean bill of health is not (compliance auditing has this shape). When a problem you face is labeled NP-complete, the right responses are: model it carefully and hand it to a commercial solver, settle for an approximation with a known quality bound, or buy a SaaS that handles it. When a problem is labeled NP-hard but the optimization version, the same options apply with looser quality guarantees.

  1. 01
    An enum is the simplest way to make the labels a thing the codebase carries. Tag each estimation function with its complexity class, and the procurement question — solver, approximation, or SaaS — falls out of the match arm.
    enum Complexity { P, NpComplete, NpHard, CoNp }
    
    fn procurement(problem: Complexity) -> &'static str {
        match problem {
            Complexity::P          => "use the standard library or crate",
            Complexity::NpComplete => "encode and dispatch to a solver",
            Complexity::NpHard     => "approximation or SaaS",
            Complexity::CoNp       => "verifier finds bugs; certifier is the hard half",
        }
    }
  2. 02
    & in a type means reference — a borrow of someone else's data instead of an owned copy. A String owns its bytes and can grow. A &str is a view into bytes that live somewhere else — you can read them, you cannot grow them, and they belong to whoever lent them to you. Functions that return strings usually return &str when the bytes already exist somewhere (a string literal, another struct's field) and only return String when fresh bytes have to be allocated.
    let owned:    String = String::from("hello");  // owns its bytes on the heap
    let borrowed: &str   = &owned;                 // a view into owned's bytes
    let literal:  &str   = "hello";                // a view into bytes baked into the binary
    
    // The "&" in a type is "reference to" — a borrow.
    // String  →  owns bytes you can grow or modify.
    // &str    →  read-only view of bytes someone else owns.
  3. 03
    'static is a lifetime annotation — a label on the borrow saying how long the referenced data stays valid. The apostrophe prefix marks every lifetime name ('a, 'b, 'static). 'static specifically means "valid for the entire run of the program." String literals like "approximation or SaaS" get 'static for free because the compiler bakes their bytes into the read-only section of the binary — they exist before main runs and after it returns. Most function signatures do not need any lifetime written — the compiler elides (fills in) lifetimes from a small set of rules. 'static is the case where elision cannot help, because the returned reference is not tied to any input, so the lifetime has to be spelled out.
    // Every arm of procurement() returns a string literal.
    // Literals live in the binary forever ⇒ their lifetime is 'static.
    // Rust cannot guess that from the inputs, so we write it explicitly.
    fn procurement(p: Complexity) -> &'static str {
        /* ... */
        "approximation or SaaS"
    }
    
    // Elision in action — no lifetimes written, compiler infers them:
    fn first_word(s: &str) -> &str { /* tied to s's lifetime */ }
    
    // Equivalent with lifetimes explicit:
    fn first_word_explicit<'a>(s: &'a str) -> &'a str { /* same thing */ }
    
    // 'static is the one common annotation that resists elision.
    // The other named lifetimes ('a, 'b, ...) tie a return reference
    // to an input reference and the compiler usually fills them in.
Garey, M., Johnson, D. (1979) Computers and Intractability — the classic catalog of which problems are which. Source →
In plain terms

When someone in a planning meeting says a problem is NP-complete, they mean: this is a known-hard problem, in a club with hundreds of other known-hard problems, and the fact that none of them has fallen to a fast algorithm in over fifty years is the strongest available evidence that none of them ever will. Treat the estimate accordingly.

NP-hard is a slightly broader label. Optimization versions of NP-complete decision problems — "what is the cheapest schedule" rather than "does a schedule under cost X exist" — are NP-hard. They are at least as hard as NP-complete and sometimes harder. Most of operations research lives here.

Co-NP is the inversion. These are problems where confirming a problem (finding a fault, a security flaw, a compliance gap) is easy but certifying the absence of problems is hard. Software verification, regulatory audit, security scanning — all have co-NP flavor. You can prove the system is broken by exhibiting one bug. Proving it is unbroken requires checking everything.

The practical implication of these labels is purchasing. When your team identifies a problem as NP-complete or NP-hard, the menu of options changes. Building a from-scratch exact solver is not on the menu — it has been tried. The options are: model the problem as integer programming and call HiGHS or CPLEX (commercial); use a SAT or constraint solver (open source); buy a domain-specific SaaS (Vroom for routing, OptaPlanner for scheduling, Gurobi for general optimization); or design an approximation that is provably good enough.

The labels are a vocabulary for procurement. Learn them and the conversation gets shorter.

← 03 Reducibility — turning one problem into another
4/39
05 What if the line collapsed →