N or P
Contents Glossary
26

Knapsack — which items fit in the budget

Knapsack is NP-complete but practically cheap when the numbers are small. One of the friendliest hard problems.

The 0/1 knapsack problem: given items each with weight and value and a budget, pick the subset that maximizes value without exceeding budget. Engineering portfolio selection, ad slot allocation, feature shipping under engineering budget, asset selection for a fund — all knapsack. Classified as NP-complete (Karp 1972), but with a critical practical caveat: the dynamic programming solution runs in time proportional to the number of items times the budget value. When the budget is small (hundreds or thousands), this is microseconds. When the budget is cryptographically large, the same algorithm is infeasible. This is called "weakly NP-complete" — hard in the worst case but tractable for typical business inputs. Knapsack also has a fully polynomial-time approximation scheme: a tunable knob trading speed for closeness to optimal. In Rust, hand-roll the DP for exact answers or use lp" data-term="good_lp">good_lp for the integer programming formulation.

  1. 01
    The classic DP runs in O(items · budget) — fast whenever the budget is a small integer. For an engineering portfolio of a hundred features and a budget in story points, this is microseconds. Reach for good_lp when the budget is huge.
    fn knapsack(weights: &[u32], values: &[u32], capacity: u32) -> u32 {
        let n = weights.len();
        let cap = capacity as usize;
        let mut dp = vec![0u32; cap + 1];
        for i in 0..n {
            let w = weights[i] as usize;
            let v = values[i];
            if w > cap { continue; }
            for c in (w..=cap).rev() {
                dp[c] = dp[c].max(dp[c - w] + v);
            }
        }
        dp[cap]
    }
  2. 02
    &[u32] is a slice of u32 values, taken by reference. Two layers: [u32] is the slice type (an unsized, runtime-length sequence of u32), and the outer & borrows it. At runtime the compiler stores this as a fat pointer — a (data pointer, length) pair — so the function knows where the data lives and how many entries are valid. &[T] is the maximally permissive read-only parameter type for any contiguous sequence: a Vec<u32> coerces via &vec, an array [u32; N] coerces via &arr, and a literal &[1, 2, 3] is one directly. Same rule as page 14 applied to numeric sequences — prefer &[T] over Vec<T> for read-only parameters. It borrows instead of demanding ownership, and it accepts every shape callers might already have.
    // &[u32] decoded:
    //
    //   & [ u32 ]
    //   ─ ─────
    //   │  │
    //   │  └── [u32] — an unsized sequence of u32 values
    //   └──── & — a shared borrow of that sequence
    
    fn knapsack(weights: &[u32], values: &[u32], capacity: u32) -> u32 { /* ... */ }
    
    
    // Every call below works because the source coerces to &[u32]:
    knapsack(&[2, 3, 4], &[3, 4, 5], 8);                 // array literal
    
    let v: Vec<u32> = vec![2, 3, 4];
    let w: Vec<u32> = vec![3, 4, 5];
    knapsack(&v, &w, 8);                                  // Vec<u32> → &[u32]
    knapsack(v.as_slice(), w.as_slice(), 8);              // explicit .as_slice()
    knapsack(&v[1..], &w[1..], 8);                        // sub-slice of a slice
    
    let arr: [u32; 3] = [2, 3, 4];
    knapsack(&arr, &[3, 4, 5], 8);                        // fixed-size array → &[u32]
    
    
    // Fat pointer — what's actually stored at runtime:
    //
    //   &[u32]
    //   ┌──────────────┬────────┐
    //   │ data pointer │ length │     16 bytes on a 64-bit target
    //   └──────────────┴────────┘
    //
    // The pointer says where the bytes live; the length tells the compiler
    // how many u32 values are valid.  Indexing is bounds-checked against
    // that length — no buffer overruns.
    
    
    // Why prefer &[T] over Vec<T> for read-only parameters:
    //   • borrows instead of demanding ownership
    //   • accepts Vec, arrays, sub-slices, literals — every shape
    //   • the signature declares "I will only read, not grow or shrink"
    //
    // Reach for Vec<T> as a parameter only when the function genuinely needs
    // to take ownership (e.g., to store or move it elsewhere).  Reach for
    // &mut [T] when it needs to mutate elements in place but not change length.
    
    
    // The slice family across this book:
    //   &[u32]      weights / values list — read-only sequence of numbers
    //   &str        a string slice (page 4) — the same idea applied to bytes
    //   &[&str]     a list of string references (page 17)
    //   &mut [T]    a writable slice — used for in-place sorting and shuffling
Karp, R. (1972). Bellman, R. (1957) for the DP. Source →
In plain terms

Knapsack is one of the friendliest NP-complete problems. Technically it is in the expensive category, but in practice it is almost always tractable for business use.

The problem: you have items, each with a weight (cost, time, capacity used) and a value. You have a budget (total cost, total time, total capacity). Pick the subset of items that maximizes total value without exceeding the budget. This shape shows up everywhere in business — engineering portfolio selection (which features to ship under a fixed engineering budget), advertising slot allocation, fund construction, server packing, freight loading.

The dynamic programming solution from 1957 fills a table where each cell represents "the maximum value achievable using only the first i items, with budget at most j." The work is proportional to the product of items and budget. For a typical business problem with a hundred items and a budget in the thousands, this is microseconds. The optimal answer comes back in negligible time.

The catch is that "budget" here means the numeric value, not the bit-length of the value. If the budget is one billion (typical for financial portfolios in dollars), the work is a hundred items times one billion — too slow. This is the technical sense in which knapsack is NP-complete: when the budget is cryptographically large, the DP becomes infeasible.

For most business uses, the budget is small enough that the DP works. When it does not, the fallback is to model the problem as an integer linear program and call HiGHS via Rust's lp" data-term="good_lp">good_lp library. This handles much larger problems in seconds.

Knapsack also has one of the best approximation algorithms in the field — a "fully polynomial-time approximation scheme" (FPTAS). You set a quality knob (95% of optimal? 99%?) and the algorithm produces a solution at that quality in time polynomial in both items and the inverse of the quality gap. This is the gold standard of approximability.

For planning: knapsack is "expensive in theory but cheap in practice" for typical business inputs. Engineering estimate is a few days for an in-house implementation or a few hours to wire up good_lp.

When the budget is huge, the problem really is expensive. When the budget is reasonable, the problem is solved. Read the numbers before sizing the work.

← 25 Traveling Salesman — the poster problem
26/39
27 Picking the right subset under constraints →