Structs & Data Structures · intermediate · ~15 min
Two-dimensional string DP.
Comparing two strings is the classic 2-D dynamic program. Build a table indexed by prefix lengths: dp[i][j] is the answer for the first i characters of one string and the first j of the other. Longest common subsequence grows the diagonal by one whenever the characters match and otherwise takes the better neighbour; edit distance (Levenshtein) counts the cheapest sequence of insertions, deletions and substitutions. The two share a table shape and differ only in the recurrence — LCS maximises a match, edit distance minimises a cost.
Every git diff, every spell-checker suggestion, every DNA sequence alignment and every fuzzy-search ranking is this algorithm. Edit distance is how a search box decides that "recieve" meant "receive", and LCS is how a diff decides which lines are unchanged. Knowing the table shape means you can adapt it — weighted edits, restricted operations, alignment output — rather than reaching for a library and hoping.
Prefix indexing. dp[i][j] covers a[0..i-1] and b[0..j-1]. Row and column 0 represent an empty prefix, which is why the table has (la+1) x (lb+1) entries — the off-by-one that trips most first attempts.
LCS recurrence. If a[i-1] == b[j-1], the characters pair up: dp[i][j] = dp[i-1][j-1] + 1. Otherwise drop one character from either string and keep the better: max(dp[i-1][j], dp[i][j-1]).
Edit-distance recurrence. Base cases are real work here: dp[i][0] = i (delete everything) and dp[0][j] = j (insert everything). Then a match costs nothing (dp[i-1][j-1]), and a mismatch is 1 + min(substitute, delete, insert).
Subsequence, not substring. LCS allows gaps — "ace" is a subsequence of "abcde". Contiguity is a different problem (see the matrix-DP lesson).
Flattened 2-D arrays. In C a (la+1) x (lb+1) table is usually one allocation indexed dp[i*W + j] with W = lb+1. Getting W wrong is the single most common source of garbage results.
Space. Each row depends only on the previous one, so two rows suffice for the score — but the full table is required to reconstruct the actual alignment.
#include <stdlib.h>
#include <string.h>
int lcs(const char *a, const char *b) {
int la = (int)strlen(a), lb = (int)strlen(b), W = lb + 1; /* row stride */
int *dp = calloc((size_t)(la + 1) * W, sizeof *dp); /* row/col 0 = empty prefix */
if (!dp) return -1;
for (int i = 1; i <= la; i++)
for (int j = 1; j <= lb; j++)
dp[i*W + j] = (a[i-1] == b[j-1])
? dp[(i-1)*W + (j-1)] + 1
: (dp[(i-1)*W + j] > dp[i*W + (j-1)]
? dp[(i-1)*W + j] : dp[i*W + (j-1)]);
int r = dp[la*W + lb];
free(dp);
return r;
}
Key points:
W = lb + 1 is the stride; every index is i*W + j.i-1 / j-1 because row 0 is the empty prefix.calloc gives the zero base row/column that LCS needs for free; edit distance must fill them explicitly.Comparing two strings gives two staples: the longest common subsequence (characters in order, gaps allowed) and the edit distance (min inserts/deletes/substitutions). Both fill a 2-D table where each cell depends on its up, left, and diagonal neighbours.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static int lcs(const char*a,const char*b){int la=strlen(a),lb=strlen(b),W=lb+1;int*dp=calloc((la+1)*W,sizeof(int));for(int i=1;i<=la;i++)for(int j=1;j<=lb;j++)dp[i*W+j]=(a[i-1]==b[j-1])?dp[(i-1)*W+j-1]+1:(dp[(i-1)*W+j]>dp[i*W+j-1]?dp[(i-1)*W+j]:dp[i*W+j-1]);int r=dp[la*W+lb];free(dp);return r;}
static int edit(const char*a,const char*b){int la=strlen(a),lb=strlen(b),W=lb+1;int*dp=malloc((la+1)*W*sizeof(int));for(int i=0;i<=la;i++)dp[i*W]=i;for(int j=0;j<=lb;j++)dp[j]=j;for(int i=1;i<=la;i++)for(int j=1;j<=lb;j++){if(a[i-1]==b[j-1])dp[i*W+j]=dp[(i-1)*W+j-1];else{int m=dp[(i-1)*W+j];if(dp[i*W+j-1]<m)m=dp[i*W+j-1];if(dp[(i-1)*W+j-1]<m)m=dp[(i-1)*W+j-1];dp[i*W+j]=m+1;}}int r=dp[la*W+lb];free(dp);return r;}
int main(void){
printf("LCS(\"ABCBDAB\",\"BDCAB\") = %d\n", lcs("ABCBDAB","BDCAB"));
printf("edit distance(\"kitten\",\"sitting\") = %d\n", edit("kitten","sitting"));
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | W = lb + 1 |
The stride. Row i starts at offset i*W. |
| 2 | calloc((la+1)*W) |
Row 0 and column 0 are all zeros — the LCS of anything with an empty string is 0. |
| 3 | a[i-1] == b[j-1] |
Compares the last characters of the two prefixes; the -1 is the prefix-to-index shift. |
| 4 | match | dp[(i-1)*W + (j-1)] + 1 — both prefixes shrink by one and the pairing adds 1. |
| 5 | mismatch | Take the max of the up and left neighbours: drop a character from a or from b. |
| 6 | dp[la*W + lb] |
The bottom-right cell is the answer. For "ABCBDAB"/"BDCABA" it is 4. |
Confusing subsequence with substring; forgetting edit distance's base row/column.
Compiler errors and warnings:
warning: comparison of integer expressions of different signedness — strlen returns size_t; cast to int once up front.-Wmaybe-uninitialized if you used malloc for LCS and never wrote row 0.Runtime symptoms:
lb + 1, not lb. This is the dominant bug.a[i] instead of a[i-1].calloc and never filled dp[i][0] = i / dp[0][j] = j; unlike LCS, edit distance needs real base cases.Technique: print the whole table for two short strings (say 4x4). The diagonal structure of an LCS table is instantly recognisable, and a wrong stride shows up as visible garbage.
(la + 1) * (lb + 1) can overflow int for long inputs — compute in size_t as shown, and bound the input lengths before allocating.calloc fails; handle NULL rather than dereferencing it.calloc's zeros; edit distance must write its base row and column or it reads meaningless zeros.dp[i*W + j] with 0 <= i <= la, 0 <= j <= lb. A mismatched stride does not fault — it silently reads the wrong cell, which is why the bug is so hard to spot.Concrete uses: git diff and diff compute a longest common subsequence over lines. Spell checkers and search engines rank suggestions by edit distance. Bioinformatics aligns DNA and protein sequences with weighted variants (Needleman-Wunsch is edit distance with a scoring matrix). Plagiarism detection, record linkage and fuzzy joins all use these scores.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) LCS length. Implement int lcs(const char *a, const char *b). Example: "ABCBDAB", "BDCABA" -> 4. Concepts: prefix table, stride.
2. (Beginner) Edit distance. Implement int edit(const char *a, const char *b) with the explicit base row/column. Example: "kitten", "sitting" -> 3. Concepts: min of three operations.
3. (Intermediate) Rolling rows. Rewrite LCS to use only two rows and confirm it matches the full-table version. Concepts: space reduction, dependency analysis.
4. (Intermediate) Reconstruct the subsequence. Extend LCS to output the actual common subsequence string. Hint: walk backwards from dp[la][lb], moving diagonally on a match. Concepts: DP reconstruction.
String comparison uses a 2-D table indexed by prefix lengths, with row and column 0 standing for the empty prefix — hence a (la+1) x (lb+1) table and a[i-1] when reading characters. LCS extends the diagonal on a match and otherwise takes the better neighbour; edit distance fills a real base row and column, then takes 1 + min(substitute, delete, insert) on a mismatch. In C the table is normally one flat allocation indexed i*W + j with W = lb + 1, and a wrong stride is the classic silent bug. Two rolling rows give the score; keep the full table when you need the alignment itself.