<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Algorithm design
All lessons in this course

Lesson 44 of 50 16 minAdvanced

Dynamic programming: knapsack, LCS and edit distance

Solve two-dimensional DP problems: the 0/1 knapsack with item reconstruction, the longest common subsequence behind diff tools, and edit distance behind spell checkers.

The hardest-looking DP problems use a two-dimensional state: “the best answer using the first i items with capacity w”, or “comparing the first i letters of one word with the first j of another”. The recipe is the same as before; the table is just a grid.

0/1 knapsack

A truck can carry 10 tonnes. Each load has a weight and a value, and each is taken whole or not at all. Which loads maximise the value? Greedy by value per tonne fails here (unlike the fractional version). State: best[i, w] = the best value using the first i loads with capacity w. For load i there are two choices: leave it (best[i − 1, w]) or take it if it fits (value + best[i − 1, w − weight]).

Knapsack.cs
(string Name, int Weight, int Value)[] loads ={    ("Rebar", 5, 10), ("Bolts", 4, 40), ("Panels", 6, 30), ("Anchors", 3, 50)};int capacity = 10, n = loads.Length; var best = new int[n + 1, capacity + 1];          // row 0: no loads, value 0for (int i = 1; i <= n; i++){    var (_, wt, val) = loads[i - 1];    for (int w = 0; w <= capacity; w++)    {        best[i, w] = best[i - 1, w];                                   // leave load i        if (wt <= w) best[i, w] = Math.Max(best[i, w], val + best[i - 1, w - wt]);   // take it    }}Console.WriteLine($"Best value: {best[n, capacity]}"); // Walk back: if the value changed from the row above, load i was takenvar taken = new List<string>();for (int i = n, w = capacity; i > 0; i--)    if (best[i, w] != best[i - 1, w]) { taken.Add(loads[i - 1].Name); w -= loads[i - 1].Weight; }taken.Reverse();Console.WriteLine(string.Join(", ", taken));
Output
Best value: 90
Bolts, Anchors

Time and memory O(n × capacity). If you only need the value, one row is enough: loop w downwards so that each load is used at most once (best[w] = max(best[w], val + best[w − wt])). Looping upwards would let a load be taken again and again, which is the “unbounded” knapsack.

Longest common subsequence

What is the longest sequence of letters that appears, in order, in both of two words? Comparing two files line by line is the same question with lines instead of letters: everything in the LCS is unchanged, everything else was added or removed. That is how diff and Git show changes.

State: L[i, j] = LCS length of the first i letters of A and the first j letters of B. If the last letters match, they extend the LCS of the shorter prefixes (diagonal + 1). If not, drop the last letter of one word or the other and take the better result.

Interactive

Comparing two words

Cell (i, j) = LCS of the first i letters of A and the first j of B. Letters equal: diagonal + 1. Otherwise: the bigger of above and left.

·STEAL
·000000
S0
T0
E0
E0
L0
Row 0 and column 0 are 0: an empty word has nothing in common with anything.
Lcs.cs
Console.WriteLine(Lcs("STEEL", "STEAL"));Console.WriteLine(Lcs("ABCBDAB", "BDCABA")); static string Lcs(string a, string b){    var L = new int[a.Length + 1, b.Length + 1];    for (int i = 1; i <= a.Length; i++)        for (int j = 1; j <= b.Length; j++)            L[i, j] = a[i - 1] == b[j - 1]                ? L[i - 1, j - 1] + 1                       // match: extend the diagonal                : Math.Max(L[i - 1, j], L[i, j - 1]);       // drop a letter from one side     var sb = new System.Text.StringBuilder();              // walk back to read the letters    for (int i = a.Length, j = b.Length; i > 0 && j > 0;)    {        if (a[i - 1] == b[j - 1]) { sb.Insert(0, a[i - 1]); i--; j--; }        else if (L[i - 1, j] >= L[i, j - 1]) i--;        else j--;    }    return $"{L[a.Length, b.Length]} \"{sb}\"";}
Output
4 "STEL"
4 "BCBA"

Edit distance

How many single-letter inserts, deletes or replacements turn one word into another? This is the Levenshtein distance. Spell checkers suggest the dictionary words at the smallest distance from what you typed; it also matches slightly misspelled names in data imports. State: D[i, j] for the first i letters of A and j of B. If the letters match, copy the diagonal; otherwise 1 + the cheapest of replace (diagonal), delete (above) and insert (left).

EditDistance.cs
Console.WriteLine(EditDistance("KITTEN", "SITTING"));Console.WriteLine(EditDistance("STEEL", "STEAL"));Console.WriteLine(EditDistance("COLUMN", "COLUMNS"));Console.WriteLine(EditDistance("BEAM", "BEAM")); static int EditDistance(string a, string b){    var D = new int[a.Length + 1, b.Length + 1];    for (int i = 0; i <= a.Length; i++) D[i, 0] = i;     // delete everything    for (int j = 0; j <= b.Length; j++) D[0, j] = j;     // insert everything    for (int i = 1; i <= a.Length; i++)        for (int j = 1; j <= b.Length; j++)            D[i, j] = a[i - 1] == b[j - 1]                ? D[i - 1, j - 1]                                      // letters match: free                : 1 + Math.Min(D[i - 1, j - 1], Math.Min(D[i - 1, j], D[i, j - 1]));   // replace, delete, insert    return D[a.Length, b.Length];}
Output
3
1
1
0
ProblemStateTimeUsed in
0/1 knapsackfirst i items, capacity wO(n · W)Loading, budgeting, cutting stock
Longest common subsequenceprefixes i and jO(n · m)diff, Git, DNA comparison
Edit distanceprefixes i and jO(n · m)Spell check, fuzzy search, data cleaning
Quick check

In the knapsack table, why does best[i, w] look at row i − 1 when taking load i?

Quick check

What is the edit distance between “BEAM” and “BEAMS”?

Practice

Can a set of loads be split into two groups with exactly equal total weight (one for each truck)? { 1, 5, 11, 5 } → True (11 and 1 + 5 + 5); { 1, 2, 3, 5 } → False. Use a one-dimensional knapsack-style table of booleans.

Show a hint

If the total is odd, it is impossible. Otherwise ask: can some subset reach total / 2? can[s] = some subset sums to s. For each load, loop s downwards from target to the load’s weight: can[s] |= can[s − weight].

Show the solution
Solution.cs
Console.WriteLine(CanSplitEvenly(new[] { 1, 5, 11, 5 }));Console.WriteLine(CanSplitEvenly(new[] { 1, 2, 3, 5 })); static bool CanSplitEvenly(int[] loads){    int total = loads.Sum();    if (total % 2 != 0) return false;    int target = total / 2;    var can = new bool[target + 1];    can[0] = true;                                  // the empty group    foreach (int w in loads)        for (int s = target; s >= w; s--)           // downwards: each load used once            can[s] |= can[s - w];    return can[target];}
Output
True
False

Remember

  • 0/1 knapsack: best[i, w] = max(leave item i, take it + best of the rest); walk back to find the items.
  • Loop capacity downwards in a 1-D table to use each item once; upwards allows repeats.
  • LCS and edit distance fill a table over two prefixes; the traceback gives the actual alignment.
  • Two-dimensional DP is O(n · m) time; often one row of memory is enough when only the value is needed.
0 of 50 lessons completed