Dynamic Programming · 17 of 60
Space Optimized Solution of LCS
Find the length of the longest common subsequence of two words while keeping only two short rows of the table instead of the whole table.
- Simple way
- time O(m * n)
room O(m * n) - Clever way
- time O(m * n)
room O(min(m, n))
The problem, simply
You get two words, for example ABCBDAB and BDCAB. A subsequence is what you get by crossing out some letters and keeping the rest in order. Find the length of the longest subsequence that both words share. For these two words it is 4 (BCAB is one such word).
The usual answer fills a big table with one row per letter of the first word. Here the goal is to get the same answer while storing only a tiny part of that table.
A story to remember it
Picture it
The full table for ABCBDAB (rows) and BDCAB (columns). To fill the highlighted row 4 (letter B), you only ever read row 3 above it. Rows 0 to 2 are dead weight.
- looking now
- seen
Examples
The classic pair
- In
s1 = 'ABCBDAB', s2 = 'BDCAB'- Out
4
BCAB and BDAB both have length 4.
Another pair
- In
s1 = 'AGGTAB', s2 = 'GXTXAYB'- Out
4
GTAB.
Nothing in common
- In
s1 = 'abc', s2 = 'def'- Out
0
No letter appears in both.
An empty word
- In
s1 = '', s2 = 'abc'- Out
0
The empty word shares nothing. Your code must not crash on it.
Same word
- In
s1 = 'abc', s2 = 'abc'- Out
3
The whole word is common.
Way 1: the simple way
Fill the full table. dp[i][j] is the answer for the first i letters of s1 and the first j letters of s2.
- Row 0 and column 0 are 0 (an empty word shares nothing).
- If the letters match,
dp[i][j] = dp[i-1][j-1] + 1, the corner plus one. - If not,
dp[i][j] = max(dp[i-1][j], dp[i][j-1]), the bigger of above and left. - The answer is
dp[m][n].
class Solution {
public int lcs(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1; // match: corner + 1
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // bigger of above and left
}
}
}
return dp[m][n];
}
}How long it takes: one step per cell, O(m * n). Extra room: the whole table, O(m * n). Two words of 10,000 letters need 100 million cells, which is about 400 MB of ints.
Way 2: the clever way
Each cell reads from three places: above, above left, and left. Above and above left are in the previous row. Left is in the row you are writing. So two rows are enough.
- Make the shorter word the columns, so each row is as short as possible.
- Keep
prev(the row above) andcurr(the row being filled). - Fill
currwith the same rule as before, readingprevfor above and corner. - After each row, swap them:
currbecomesprev, and the oldprevis reused as the newcurr.
Two rows sliding down the table
01 / 12
Row 0 means no letters of the first word yet, so every value is 0. That is our prev row. The curr row is about to be filled for the letter A.
- rows kept
- 2
- looking now
- checking
- seen
- done
- answer
- just added
class Solution {
public int lcs(String s1, String s2) {
if (s1.length() < s2.length()) { // make s2 the shorter word
String t = s1; s1 = s2; s2 = t;
}
int m = s1.length(), n = s2.length();
int[] prev = new int[n + 1]; // the row above
int[] curr = new int[n + 1]; // the row we are writing
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
curr[j] = prev[j - 1] + 1; // corner + 1
} else {
curr[j] = Math.max(prev[j], curr[j - 1]); // above or left
}
}
int[] t = prev; prev = curr; curr = t; // slide down
}
return prev[n]; // after the last swap, prev is the last row
}
}How long it takes: still one step per cell, O(m * n). Extra room: two rows of the shorter word's length, O(min(m, n)).
Way 3: one row and a saved corner
You can even use one row. Write the new row on top of the old one, from left to right. The only problem is the corner: by the time you need dp[i-1][j-1], you have already overwritten it. So save it in a variable before you overwrite each cell.
class Solution {
public int lcs(String s1, String s2) {
if (s1.length() < s2.length()) {
String t = s1; s1 = s2; s2 = t;
}
int m = s1.length(), n = s2.length();
int[] dp = new int[n + 1];
for (int i = 1; i <= m; i++) {
int corner = 0; // dp[i-1][j-1], saved before it is lost
for (int j = 1; j <= n; j++) {
int above = dp[j]; // still the old row's value
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[j] = corner + 1;
} else {
dp[j] = Math.max(above, dp[j - 1]);
}
corner = above; // becomes the corner for column j + 1
}
}
return dp[n];
}
}Same O(m * n) time, and only n + 1 numbers plus one saved corner.
Watch out for
- In the one row version, forgetting to save the corner. Then a match reads the new value instead of the old one, and answers come out too big.
- Forgetting to reset
corner = 0at the start of each row. Column 0 is always 0, so the corner must start at 0 again, or a value from the previous row leaks in. - Pick the shorter word for the columns, or the room used is the longer word's length.
- With two rows you can only get the length. To print the actual subsequence you need more (see below).
What interviewers ask next
- Can you print the subsequence with little memory? Yes, with Hirschberg's trick. Split the first word in half, run the two row pass forward on the top half and backward on the bottom half, find the best split column, and recurse on the two halves. Time stays
O(m * n)and room isO(min(m, n)). - Does this trick work for edit distance too? Yes. Edit distance reads the same three neighbors, so the same two rows work.
- What about three words? The table is 3D, and you can keep two 2D layers instead of the whole cube.
- When is LCS faster than
O(m * n)? When letters are rare. Special methods based on matching pairs (Hunt and Szymanski) are fast when few letters match.
Remember this
Grown-up words: crossing out letters gives a subsequence, the chart is the LCS dynamic programming table, keeping two lines is the rolling array trick, the saved corner is the diagonal variable, and the halving trick is Hirschberg's algorithm.