Longest Common Substring in DSA Python - Time & Space Complexity
We want to understand how the time needed to find the longest common substring grows as the input strings get longer.
Specifically, how does the work increase when comparing two strings of different lengths?
Analyze the time complexity of the following code snippet.
def longest_common_substring(s1: str, s2: str) -> int:
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
max_len = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
max_len = max(max_len, dp[i][j])
return max_len
This code finds the length of the longest substring common to both input strings using a 2D table.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Nested loops comparing characters of both strings and updating the table.
- How many times: The inner loop runs for each character of the second string for every character of the first string, so m x n times.
As the lengths of the two strings increase, the number of comparisons grows by multiplying their lengths.
| Input Size (m, n) | Approx. Operations |
|---|---|
| 10, 10 | 100 |
| 100, 100 | 10,000 |
| 1000, 1000 | 1,000,000 |
Pattern observation: Doubling the length of both strings roughly quadruples the work because every character in one string is compared to every character in the other.
Time Complexity: O(m x n)
This means the time needed grows proportionally to the product of the lengths of the two strings.
[X] Wrong: "The algorithm only depends on the length of the shorter string, so it runs in O(min(m, n)) time."
[OK] Correct: The nested loops compare every character of one string to every character of the other, so both lengths matter and multiply the work.
Understanding this time complexity helps you explain how dynamic programming solves problems by breaking them into smaller parts and why nested loops increase work.
"What if we used a suffix tree or suffix automaton instead? How would the time complexity change?"
Practice
Solution
Step 1: Understand the problem definition
The Longest Common Substring problem looks for the longest continuous sequence of characters that appears in both strings.Step 2: Differentiate from similar problems
Unlike Longest Common Subsequence, substrings must be contiguous, so only continuous matching parts count.Final Answer:
The longest sequence of characters that appears contiguously in both strings -> Option BQuick Check:
Longest Common Substring = continuous matching part [OK]
- Confusing substring with subsequence
- Thinking order doesn't matter
- Counting non-contiguous matches
Solution
Step 1: Understand 2D list initialization
We need a list of lists with (m+1) rows and (n+1) columns filled with zeros.Step 2: Check each option
dp = [[0] * (n+1) for _ in range(m+1)] uses list comprehension correctly to create independent rows. dp = [[0] * (m+1)] * (n+1) creates shallow copies causing shared rows. dp = [[0 for _ in range(m+1)] for _ in range(n+1)] swaps m and n sizes. dp = [0] * (m+1) * (n+1) creates a flat list, not 2D.Final Answer:
dp = [[0] * (n+1) for _ in range(m+1)] -> Option CQuick Check:
Use list comprehension for independent rows [OK]
- Using multiplication for nested lists causing shared references
- Swapping row and column sizes
- Creating 1D list instead of 2D
def longest_common_substring(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n+1) for _ in range(m+1)]
max_len = 0
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
max_len = max(max_len, dp[i][j])
return max_len
print(longest_common_substring('abcde', 'abfde'))Solution
Step 1: Trace matching characters
Comparing 'abcde' and 'abfde': 'a' matches 'a', 'b' matches 'b', 'c' vs 'f' no match, 'd' matches 'd', 'e' matches 'e'. The longest continuous match is 'de' (length 2) at the end, but 'abc' vs 'abf' only matches 'ab' (length 2). However, the longest continuous substring is 'de' (length 2) or 'ab' (length 2). But the code finds the longest continuous substring length, which is 2.Step 2: Confirm longest substring length
Actually, the longest continuous substring is 'de' or 'ab' both length 2, so the output is 2.Final Answer:
2 -> Option AQuick Check:
Longest continuous match = 2 [OK]
- Counting non-continuous matches
- Assuming all matching chars form one substring
- Off-by-one errors in indexing
def lcs_bug(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (m+1) for _ in range(n+1)]
max_len = 0
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
max_len = max(max_len, dp[i][j])
return max_len
print(lcs_bug('abc', 'abc'))Solution
Step 1: Check dp array initialization
dp is initialized with dimensions (n+1) x (m+1), but loops use i in [1..m], j in [1..n], causing index mismatch.Step 2: Identify impact of swapped dimensions
Accessing dp[i][j] where i can be up to m and j up to n will cause index errors or incorrect results because dp rows and columns are swapped.Final Answer:
The dp array dimensions are swapped causing index errors -> Option DQuick Check:
dp size must match loop indices [OK]
- Swapping row and column sizes in dp
- Starting loops at 0 causing index errors
- Not updating max_len inside condition
s1 = 'ababc' and s2 = 'babca', which of the following Python functions correctly returns the longest common substring itself (not just length)?Solution
Step 1: Analyze def lcs_string(s1, s2): m, n = len(s1), len(s2) dp = [[0]*(n+1) for _ in range(m+1)] max_len = 0 end = 0 for i in range(1, m+1): for j in range(1, n+1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 if dp[i][j] > max_len: max_len = dp[i][j] end = i return s1[end-max_len:end] logic
def lcs_string(s1, s2): m, n = len(s1), len(s2) dp = [[0]*(n+1) for _ in range(m+1)] max_len = 0 end = 0 for i in range(1, m+1): for j in range(1, n+1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 if dp[i][j] > max_len: max_len = dp[i][j] end = i return s1[end-max_len:end] uses dynamic programming to track lengths of matching substrings and records the end index of the longest one, then slices s1 to return it.Step 2: Check other options for correctness
def lcs_string(s1, s2): return max(set(s1) & set(s2)) returns a single character, not substring. def lcs_string(s1, s2): return s1 if s1 in s2 else '' returns s1 only if fully contained in s2. def lcs_string(s1, s2): return ''.join([c for c in s1 if c in s2]) returns all matching chars concatenated, not continuous substring.Final Answer:
Option A correctly returns the longest common substring -> Option AQuick Check:
DP with end index tracks substring [OK]
- Returning only length, not substring
- Using set intersection losing order and continuity
- Concatenating matching chars ignoring continuity
Ready to prove it?
Pass the test and this pattern moves unknown
