🧠
Top-Down DP with Memoization
💡 This approach uses recursion with memoization to avoid recomputing overlapping subproblems, improving efficiency over brute force but still not optimal for very large inputs.
Intuition
Define a recursive function that returns the length of the longest common substring ending at indices i and j. Use memoization to store results and avoid recomputation.
Algorithm
- Define a recursive function lcs(i, j) that returns longest common substring length ending at s1[i-1], s2[j-1].
- If characters match, return 1 + lcs(i-1, j-1), else 0.
- Use a memo dictionary to store computed results for (i, j).
- Iterate over all i, j pairs to find the maximum value returned by lcs.
💡 The recursion with memoization is tricky because it only counts substrings ending at specific indices, so we must check all pairs.
Recurrence:dp(i,j) = dp(i-1,j-1) + 1 if s1[i-1] == s2[j-1] else 0
def longest_common_substring_memo(s1, s2):
n, m = len(s1), len(s2)
memo = {}
max_len = 0
def lcs(i, j):
if i == 0 or j == 0:
return 0
if (i, j) in memo:
return memo[(i, j)]
if s1[i-1] == s2[j-1]:
memo[(i, j)] = 1 + lcs(i-1, j-1)
else:
memo[(i, j)] = 0
return memo[(i, j)]
for i in range(1, n+1):
for j in range(1, m+1):
length = lcs(i, j)
if length > max_len:
max_len = length
return max_len
# Driver code
if __name__ == '__main__':
print(longest_common_substring_memo('abcde', 'abfce')) # Output: 2
Line Notes
memo = {}Cache to store results of subproblems to avoid recomputation
def lcs(i, j):Recursive function to compute longest substring ending at i-1, j-1
if s1[i-1] == s2[j-1]:Check if characters match to extend substring
for i in range(1, n+1):Iterate over all indices to find maximum substring length
import java.util.*;
public class LongestCommonSubstringMemo {
static Map<String, Integer> memo = new HashMap<>();
public static int lcs(String s1, String s2, int i, int j) {
if (i == 0 || j == 0) return 0;
String key = i + "," + j;
if (memo.containsKey(key)) return memo.get(key);
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
int val = 1 + lcs(s1, s2, i - 1, j - 1);
memo.put(key, val);
return val;
} else {
memo.put(key, 0);
return 0;
}
}
public static int longestCommonSubstring(String s1, String s2) {
int maxLen = 0;
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <= s2.length(); j++) {
int length = lcs(s1, s2, i, j);
if (length > maxLen) maxLen = length;
}
}
return maxLen;
}
public static void main(String[] args) {
System.out.println(longestCommonSubstring("abcde", "abfce")); // Output: 2
}
}
Line Notes
static Map<String, Integer> memo = new HashMap<>();Memoization cache to store subproblem results
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {Check character match to extend substring
for (int i = 1; i <= s1.length(); i++) {Iterate over all indices to find max substring length
String key = i + "," + j;Unique key for memoization map
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
unordered_map<string, int> memo;
int lcs(const string& s1, const string& s2, int i, int j) {
if (i == 0 || j == 0) return 0;
string key = to_string(i) + "," + to_string(j);
if (memo.find(key) != memo.end()) return memo[key];
if (s1[i - 1] == s2[j - 1]) {
memo[key] = 1 + lcs(s1, s2, i - 1, j - 1);
} else {
memo[key] = 0;
}
return memo[key];
}
int longestCommonSubstringMemo(const string& s1, const string& s2) {
int maxLen = 0;
for (int i = 1; i <= (int)s1.size(); i++) {
for (int j = 1; j <= (int)s2.size(); j++) {
int length = lcs(s1, s2, i, j);
if (length > maxLen) maxLen = length;
}
}
return maxLen;
}
int main() {
cout << longestCommonSubstringMemo("abcde", "abfce") << endl; // Output: 2
return 0;
}
Line Notes
unordered_map<string, int> memo;Memo cache to store results of subproblems
if (s1[i - 1] == s2[j - 1]) {Check if characters match to extend substring
for (int i = 1; i <= (int)s1.size(); i++) {Iterate over all indices to find max substring length
string key = to_string(i) + "," + to_string(j);Create unique key for memoization
function longestCommonSubstringMemo(s1, s2) {
const memo = new Map();
const n = s1.length, m = s2.length;
let maxLen = 0;
function lcs(i, j) {
if (i === 0 || j === 0) return 0;
const key = i + ',' + j;
if (memo.has(key)) return memo.get(key);
if (s1[i - 1] === s2[j - 1]) {
const val = 1 + lcs(i - 1, j - 1);
memo.set(key, val);
return val;
} else {
memo.set(key, 0);
return 0;
}
}
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
const length = lcs(i, j);
if (length > maxLen) maxLen = length;
}
}
return maxLen;
}
// Test
console.log(longestCommonSubstringMemo('abcde', 'abfce')); // Output: 2
Line Notes
const memo = new Map();Memoization cache to avoid recomputation
if (s1[i - 1] === s2[j - 1]) {Check if characters match to extend substring
for (let i = 1; i <= n; i++) {Iterate over all indices to find max substring length
const key = i + ',' + j;Unique key for memo map
TimeO(n*m) due to memoization avoiding repeated work
SpaceO(n*m) for memoization cache and recursion stack
Each pair (i,j) computed once, total n*m states.
💡 For n=m=1000, about 1 million operations, which is feasible.
Interview Verdict: Accepted but can be improved with bottom-up DP
Better than brute force but recursion overhead and memory usage can be high.