Rust for Algorithmic Problem-Solving

Dynamic Programming

Dynamic Programming (DP) solves complex optimization and counting problems by breaking them into overlapping subproblems with optimal substructure and storing each subproblem's result in a table so it is computed only once. In Rust, bottom-up iterative tabulation with flat Vec slices and std::mem::swap rolling rows avoids recursion-limit stack overflows and maximizes L1 cache speed.

2D DP Table Space Optimization (`std::mem::swap(&mut prev_row, &mut curr_row)`):

  Full 2D Table (`N x M` memory):         Rolling 2-Row DP (`2 x M` memory!):
  Row 0: [ .  .  .  .  . ]
  Row 1: [ .  .  .  .  . ]                `prev_row`: [ p0, p1, p2, p3, p4 ]
  ...                                                       |   \
  Row i: [ .  .  .  .  . ]                `curr_row`: [ c0, c1, c2, c3, c4 ]
                                          After finishing row: `mem::swap(&mut prev, &mut curr)`!

Comparing Top-Down Memoization and Bottom-Up Tabulation in Rust

Approach Implementation in Rust Pros Cons
Top-Down Memoization Recursive fn solve(state, memo: &mut Vec<Option<u64>>) Only visits reachable states when state space is sparse. Deep recursion can overflow the call stack for $N \ge 10^5$.
Bottom-Up Tabulation Iterative for loops over dp: Vec<u64> Zero recursion overhead, auto-vectorizable, and supports $O(M)$ rolling-row space reduction! Evaluates all states in topological order.

1. Space-Optimized 1D Knapsack DP ($O(N \cdot W)$ Time, $O(W)$ Memory)

In the 0/1 Knapsack problem, iterating capacities backward (for w in (weight..=capacity).rev()) allows updating a single 1D dp vector in place without overwriting values from the previous item row:

/// Solves the 0/1 Knapsack problem in O(N * W) time and O(W) space using a 1D DP array.
pub fn knapsack_01(items: &[(usize, u64)], capacity: usize) -> u64 {
    let mut dp = vec![0u64; capacity + 1];

    for &(weight, value) in items {
        if weight <= capacity {
            // Iterate BACKWARD (`rev()`) so each item is used at most once!
            for w in (weight..=capacity).rev() {
                dp[w] = dp[w].max(dp[w - weight] + value);
            }
        }
    }

    dp[capacity]
}

2. Rolling-Row 2D DP with std::mem::swap (Levenshtein Edit Distance)

When computing a 2D DP table where row i only depends on row i - 1 (such as Longest Common Subsequence or Levenshtein Edit Distance), allocate two 1D vectors prev and curr and swap their pointers at the end of each row in $O(1)$ time with std::mem::swap(&mut prev, &mut curr):

/// Computes the Levenshtein edit distance between `a` and `b` in O(N * M) time
/// and O(min(N, M)) memory using rolling rows and `std::mem::swap`.
pub fn edit_distance(a: &str, b: &str) -> usize {
    let a = a.as_bytes();
    let b = b.as_bytes();

    let mut prev: Vec<usize> = (0..=b.len()).collect();
    let mut curr: Vec<usize> = vec![0; b.len() + 1];

    for (i, &ch_a) in a.iter().enumerate() {
        curr[0] = i + 1;
        for (j, &ch_b) in b.iter().enumerate() {
            let cost = usize::from(ch_a != ch_b);
            curr[j + 1] = (prev[j + 1] + 1)        // Deletion
                .min(curr[j] + 1)                  // Insertion
                .min(prev[j] + cost);              // Substitution / Match
        }
        // Swap `prev` and `curr` in O(1) time without reallocating!
        std::mem::swap(&mut prev, &mut curr);
    }

    prev[b.len()]
}

fn main() {
    // Items: (weight, value)
    let items = [(2, 30), (3, 50), (4, 70), (5, 80)];
    let best_val = knapsack_01(&items, 7);
    assert_eq!(best_val, 120); // weight 3 (50) + weight 4 (70) = 120

    let dist = edit_distance("kitten", "sitting");
    assert_eq!(dist, 3);

    println!("0/1 Knapsack max value (cap=7): {best_val}, Edit distance: {dist}");
}
0/1 Knapsack max value (cap=7): 120, Edit distance: 3