26. Longest Common KB Snippet
Problem
Fin's knowledge base ingestion pipeline flags likely duplicate or near-duplicate articles before they reach the retrieval index, since duplicate KB content degrades retrieval quality and reranking signal. One heuristic: tokenize each article and measure the length of the longest common (not necessarily contiguous) subsequence of shared tokens between the two token lists. A high overlap suggests the two articles cover the same guidance and should be flagged for review or merging.
Given two token lists a and b, return the length of their
longest common subsequence — the longest sequence of tokens that appears, in order
but not necessarily contiguously, in both lists.
Source: src/26_longest_common_kb_snippet.py
def lcs_length(a: list[str], b: list[str]) -> int: ...
lcs_length(["reset", "your", "password", "now"], ["how", "to", "reset", "password"])
# -> 2 ("reset", "password" appear in order in both)
lcs_length(["a", "b", "c"], ["a", "b", "c"])
# -> 3
Step-by-Step Approach
- Let
n = len(a),m = len(b). Build a(n+1) x (m+1)DP tabledp, wheredp[i][j]is the LCS length of the prefixesa[:i]andb[:j]. - Initialize row 0 and column 0 to all zeros — an empty prefix has no common subsequence with anything.
- Fill the table left-to-right, top-to-bottom for
ifrom 1 tonandjfrom 1 tom. - If
a[i-1] == b[j-1](the current tokens match), the LCS can extend the best subsequence found without either of these two tokens: setdp[i][j] = dp[i-1][j-1] + 1. - Otherwise, the current tokens can't both be part of the LCS together, so take
the best of dropping one or the other:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]). - The answer is
dp[n][m], the LCS length of the full lists.
The key insight is that the LCS of two prefixes only depends on the LCS of smaller
prefixes, so a bottom-up table avoids the exponential blowup of trying every
subsequence directly. Time and space are both O(n * m).
Reference solution
def lcs_length(a: list[str], b: list[str]) -> int:
# bottom-up DP table, dp[i][j] = LCS length of a[:i] and b[:j], O(len(a)*len(b)) time/space
n, m = len(a), len(b)
# fresh list per row, avoids aliasing bug
dp = [[0] * (m + 1) for _ in range(n + 1)]
# 1-indexed: dp[i] aligns to a[i-1]
for i in range(1, n + 1):
# dp[j] aligns to b[j-1]
for j in range(1, m + 1):
if a[i - 1] == b[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# drop from a or b, keep the better
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[n][m]
Key Functions & Tricks
[[0] * (m + 1) for _ in range(n + 1)]— builds independent per-row lists, avoiding the aliasing bug of[[0]*(m+1)]*(n+1).range(1, n + 1)— 1-indexes the DP table whilea/bstay 0-indexed.max(dp[i - 1][j], dp[i][j - 1])— picks the best LCS length after dropping one token.
How to Recognize This Pattern
Signals: the problem asks for the longest (or shortest) way to align two sequences allowing "gaps" — subsequence problems, edit distance, diff-style comparisons, or anything phrased as "not necessarily contiguous". If you see two 1-D inputs and a question about their shared/aligned structure, reach for a 2-D DP table indexed by prefix lengths of each input.
Variations: return the actual common subsequence (not just its length) by
backtracking through the table from dp[n][m]; or ask for edit
distance, which uses the same table shape but different transition rules (insert,
delete, substitute costs).
Common pitfall: confusing "common subsequence" with "common substring" — a
substring must be contiguous, which is a different (and easier) recurrence. Also
watch off-by-one errors indexing a[i-1]/b[j-1] against a
table sized (n+1) x (m+1).