๐Ÿฆ€ Functional Rust

785. 0/1 Knapsack: DP Table Approach

Difficulty: 3 Level: Intermediate Classic resource-allocation DP: given items with weights and values, maximise total value within a weight budget โ€” with three implementations and full traceback.

The Problem This Solves

The 0/1 knapsack problem appears wherever you must make binary include/exclude decisions under a resource constraint. Scheduling jobs within a time budget, selecting features within a sprint velocity, packing cargo within weight limits โ€” all are knapsack variants. The "0/1" means each item is taken whole or not at all (unlike the fractional knapsack, which is solvable greedily). This is also the canonical introduction to 2D DP tables: the recurrence `dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight_i] + value_i)` captures the exact structure of "take or skip" decisions at each step. Understanding this table and its traceback unlocks most other combinatorial DP problems.

The Intuition

Work backwards: at each item, you either take it (gaining its value, spending its weight) or skip it. The DP table stores the best achievable value for every `(item_count, remaining_capacity)` pair. Fill it left-to-right, row by row. The answer is at `dp[n][capacity]`. Traceback recovers which items were selected by walking backwards through the table. In OCaml you'd naturally write the recursive version first and add a `Hashtbl` for memoisation; in Rust, the tabulation approach with a `Vec<Vec<u64>>` is more cache-friendly and lets you traceback directly.

How It Works in Rust

// O(n ร— capacity) time, O(n ร— capacity) space
pub fn knapsack_tab(items: &[Item], capacity: usize) -> KnapsackResult {
 let n = items.len();
 // dp[i][w] = max value using first i items with capacity w
 let mut dp = vec![vec![0u64; capacity + 1]; n + 1];

 for i in 1..=n {
     let item = &items[i - 1];
     for w in 0..=capacity {
         dp[i][w] = if item.weight > w {
             dp[i-1][w]                                    // can't take it
         } else {
             dp[i-1][w].max(item.value + dp[i-1][w - item.weight]) // take or skip
         };
     }
 }

 // Traceback: walk backwards to find selected items
 let mut selected = Vec::new();
 let mut w = capacity;
 for i in (1..=n).rev() {
     if dp[i][w] != dp[i-1][w] {       // value changed โ†’ item i was taken
         selected.push(i - 1);
         w = w.saturating_sub(items[i-1].weight);
     }
 }
 // ...
}

// Space-optimised: 1D rolling array โ€” O(capacity) space
// Must iterate weight right-to-left to avoid using an item twice
pub fn knapsack_1d(items: &[Item], capacity: usize) -> u64 {
 let mut dp = vec![0u64; capacity + 1];
 for item in items {
     for w in (item.weight..=capacity).rev() {   // โ† right-to-left is critical
         dp[w] = dp[w].max(item.value + dp[w - item.weight]);
     }
 }
 dp[capacity]
}
The 1D optimisation reduces space from O(n ร— W) to O(W). The right-to-left iteration ensures each item is considered only once (if you iterate left-to-right, you'd allow taking the same item multiple times โ€” that's the unbounded knapsack variant).

What This Unlocks

Key Differences

ConceptOCamlRust
DP table`Array.make_matrix` (mutable) or recursive + `Hashtbl``vec![vec![0u64; W+1]; n+1]`
Take/skip recurrence`max` on recursive calls`dp[i-1][w].max(value + dp[i-1][w-wt])`
TracebackRecursive descent through memo tableReverse loop over 2D `Vec`
Space optimisationSwap two arrays each rowSingle `Vec`, iterate `rev()`
Item structRecord type with fields`struct Item { weight: usize, value: u64 }`
// 785. 0/1 Knapsack: DP Table Approach
// Recursive memoised + iterative tabulation + traceback

use std::collections::HashMap;

// โ”€โ”€ Item type โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, Clone)]
pub struct Item {
    pub name: &'static str,
    pub weight: usize,
    pub value: u64,
}

// โ”€โ”€ 1. Recursive with memoisation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

fn knapsack_memo_inner(
    items: &[Item],
    i: usize,
    w: usize,
    memo: &mut HashMap<(usize, usize), u64>,
) -> u64 {
    if i == 0 || w == 0 { return 0; }
    if let Some(&v) = memo.get(&(i, w)) { return v; }
    let item = &items[i - 1];
    let best = if item.weight > w {
        knapsack_memo_inner(items, i - 1, w, memo)
    } else {
        let take   = item.value + knapsack_memo_inner(items, i - 1, w - item.weight, memo);
        let skip   = knapsack_memo_inner(items, i - 1, w, memo);
        take.max(skip)
    };
    memo.insert((i, w), best);
    best
}

pub fn knapsack_memo(items: &[Item], capacity: usize) -> u64 {
    knapsack_memo_inner(items, items.len(), capacity, &mut HashMap::new())
}

// โ”€โ”€ 2. Bottom-up tabulation with traceback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub struct KnapsackResult {
    pub max_value: u64,
    pub selected: Vec<usize>,  // indices of selected items
}

pub fn knapsack_tab(items: &[Item], capacity: usize) -> KnapsackResult {
    let n = items.len();
    // dp[i][w] = max value using first i items with capacity w
    let mut dp = vec![vec![0u64; capacity + 1]; n + 1];

    for i in 1..=n {
        let item = &items[i - 1];
        for w in 0..=capacity {
            dp[i][w] = if item.weight > w {
                dp[i-1][w]
            } else {
                dp[i-1][w].max(item.value + dp[i-1][w - item.weight])
            };
        }
    }

    // Traceback: reconstruct which items were selected
    let mut selected = Vec::new();
    let mut w = capacity;
    for i in (1..=n).rev() {
        if dp[i][w] != dp[i-1][w] {
            selected.push(i - 1); // 0-indexed
            w = w.saturating_sub(items[i-1].weight);
        }
    }
    selected.reverse();

    KnapsackResult {
        max_value: dp[n][capacity],
        selected,
    }
}

// โ”€โ”€ 3. Space-optimised (1D rolling array) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub fn knapsack_1d(items: &[Item], capacity: usize) -> u64 {
    let mut dp = vec![0u64; capacity + 1];
    for item in items {
        // Iterate right-to-left to avoid using an item twice
        for w in (item.weight..=capacity).rev() {
            dp[w] = dp[w].max(item.value + dp[w - item.weight]);
        }
    }
    dp[capacity]
}

fn main() {
    let items = vec![
        Item { name: "camera", weight: 2, value: 6  },
        Item { name: "laptop", weight: 2, value: 10 },
        Item { name: "guitar", weight: 3, value: 12 },
        Item { name: "book",   weight: 1, value: 4  },
        Item { name: "drone",  weight: 4, value: 15 },
    ];
    let capacity = 7;

    println!("Items: {} items, capacity={capacity}", items.len());
    for item in &items { println!("  {:8}: w={}, v={}", item.name, item.weight, item.value); }

    let memo_val = knapsack_memo(&items, capacity);
    println!("\nMemo  best value: {memo_val}");

    let result = knapsack_tab(&items, capacity);
    println!("Table best value: {}", result.max_value);
    println!("Selected items:");
    let mut total_w = 0;
    let mut total_v = 0u64;
    for &i in &result.selected {
        println!("  {:8}: w={}, v={}", items[i].name, items[i].weight, items[i].value);
        total_w += items[i].weight;
        total_v += items[i].value;
    }
    println!("Total weight: {total_w}/{capacity}, total value: {total_v}");

    let opt_val = knapsack_1d(&items, capacity);
    println!("\n1D (space-optimised) best value: {opt_val}");

    // All three methods agree
    assert_eq!(memo_val, result.max_value);
    assert_eq!(memo_val, opt_val);
    println!("\nAll three methods agree โœ“");
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_items() -> Vec<Item> {
        vec![
            Item { name: "a", weight: 2, value: 6  },
            Item { name: "b", weight: 2, value: 10 },
            Item { name: "c", weight: 3, value: 12 },
        ]
    }

    #[test]
    fn all_methods_agree() {
        let items = test_items();
        let cap = 5;
        let m = knapsack_memo(&items, cap);
        let t = knapsack_tab(&items, cap).max_value;
        let o = knapsack_1d(&items, cap);
        assert_eq!(m, t);
        assert_eq!(t, o);
    }

    #[test]
    fn empty_knapsack() {
        let items: Vec<Item> = vec![];
        assert_eq!(knapsack_1d(&items, 10), 0);
    }

    #[test]
    fn zero_capacity() {
        let items = test_items();
        assert_eq!(knapsack_1d(&items, 0), 0);
    }

    #[test]
    fn known_value() {
        let items = vec![
            Item { name: "x", weight: 1, value: 1 },
            Item { name: "y", weight: 1, value: 1 },
        ];
        // capacity=1: take one item, value=1
        assert_eq!(knapsack_1d(&items, 1), 1);
        // capacity=2: take both, value=2
        assert_eq!(knapsack_1d(&items, 2), 2);
    }
}
(* 0/1 Knapsack in OCaml โ€” recursive + memoisation + tabulation *)

type item = { weight: int; value: int; name: string }

(* โ”€โ”€ Recursive with memoisation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)
let solve_memo items capacity =
  let n = Array.length items in
  let memo = Hashtbl.create 64 in
  let rec dp i w =
    if i = n || w = 0 then 0
    else match Hashtbl.find_opt memo (i, w) with
    | Some v -> v
    | None ->
      let item = items.(i) in
      let best =
        if item.weight > w then dp (i+1) w
        else max (dp (i+1) w) (item.value + dp (i+1) (w - item.weight))
      in
      Hashtbl.replace memo (i, w) best;
      best
  in
  dp 0 capacity

(* โ”€โ”€ Tabulation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)
let solve_tab items capacity =
  let n = Array.length items in
  (* dp.(i).(w) = max value using first i items with capacity w *)
  let dp = Array.make_matrix (n+1) (capacity+1) 0 in
  for i = 1 to n do
    let item = items.(i-1) in
    for w = 0 to capacity do
      dp.(i).(w) <-
        if item.weight > w then dp.(i-1).(w)
        else max dp.(i-1).(w) (item.value + dp.(i-1).(w - item.weight))
    done
  done;
  (* Traceback *)
  let selected = ref [] in
  let w = ref capacity in
  for i = n downto 1 do
    if dp.(i).(!w) <> dp.(i-1).(!w) then begin
      selected := items.(i-1) :: !selected;
      w := !w - items.(i-1).weight
    end
  done;
  dp.(n).(capacity), !selected

let () =
  let items = [|
    { weight=2; value=6;  name="camera"  };
    { weight=2; value=10; name="laptop"  };
    { weight=3; value=12; name="guitar"  };
    { weight=1; value=4;  name="book"    };
    { weight=4; value=15; name="drone"   };
  |] in
  let capacity = 7 in
  let best_memo = solve_memo items capacity in
  Printf.printf "Memo  best value: %d\n" best_memo;
  let (best_tab, selected) = solve_tab items capacity in
  Printf.printf "Table best value: %d\n" best_tab;
  Printf.printf "Selected items:\n";
  List.iter (fun item ->
    Printf.printf "  %s (w=%d, v=%d)\n" item.name item.weight item.value
  ) selected