beam: add LCS module with tests (length, reconstruction, diff)

This commit is contained in:
beam committed 2026-10-22 18:48:06 +00:00
1 parent c52b2f13fa
commit af94b3fdd7
2 files changed
+177

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
"""Longest common subsequence (LCS) utilities.
Provides:
- lcs_length(a, b): length of the LCS of two sequences.
- lcs(a, b): one LCS string/list (for hashable items).
- lcs_diff(a, b): minimal edit script as list of (op, item) where
op is 'keep' / 'delete' (in a only) / 'insert' (in b only).
All inputs are treated as sequences of hashable items. Time O(|a|*|b|),
space O(min(|a|, |b|)) for length and O(|a|*|b|) for reconstruction.
"""
from __future__ import annotations
from typing import Sequence, TypeVar
T = TypeVar("T")
def _validate(seq, name):
try:
iter(seq)
except TypeError as e:
raise TypeError(f"{name} must be iterable") from e
# reject strings? No—strings are sequences of chars, allowed.
def lcs_length(a: Sequence[T], b: Sequence[T]) -> int:
"""Return length of longest common subsequence of a and b."""
_validate(a, "a")
_validate(b, "b")
a = list(a)
b = list(b)
# ensure b is the shorter axis for space
if len(a) < len(b):
a, b = b, a
# now a longer (or equal), b shorter
prev = [0] * (len(b) + 1)
cur = [0] * (len(b) + 1)
for x in a:
for j, y in enumerate(b, 1):
if x == y:
cur[j] = prev[j - 1] + 1
else:
cur[j] = prev[j] if prev[j] >= cur[j - 1] else cur[j - 1]
prev, cur = cur, [0] * (len(b) + 1)
return prev[len(b)]
def _lcs_table(a, b):
m, n = len(a), len(b)
# dp[i][j] = lcs length of a[:i], b[:j]
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
ai = a[i - 1]
row = dp[i]
prow = dp[i - 1]
for j in range(1, n + 1):
if ai == b[j - 1]:
row[j] = prow[j - 1] + 1
else:
# max(prow[j], row[j-1])
v = prow[j]
if row[j - 1] > v:
v = row[j - 1]
row[j] = v
return dp
def lcs(a: Sequence[T], b: Sequence[T]) -> list[T]:
"""Return one LCS of a and b as a list (preserving item type via list)."""
_validate(a, "a")
_validate(b, "b")
a = list(a)
b = list(b)
dp = _lcs_table(a, b)
# backtrack
i, j = len(a), len(b)
out = []
while i > 0 and j > 0:
if a[i - 1] == b[j - 1]:
out.append(a[i - 1])
i -= 1
j -= 1
elif dp[i - 1][j] >= dp[i][j - 1]:
i -= 1
else:
j -= 1
out.reverse()
return out
def lcs_diff(a: Sequence[T], b: Sequence[T]) -> list[tuple[str, T]]:
"""Minimal edit script turning a into b. Ops: keep/delete/insert."""
_validate(a, "a")
_validate(b, "b")
a = list(a)
b = list(b)
dp = _lcs_table(a, b)
# walk to produce ops
i, j = len(a), len(b)
ops_rev = []
while i > 0 or j > 0:
if i > 0 and j > 0 and a[i - 1] == b[j - 1] and dp[i][j] == dp[i - 1][j - 1] + 1:
ops_rev.append(("keep", a[i - 1]))
i -= 1
j -= 1
elif i > 0 and (j == 0 or dp[i - 1][j] >= dp[i][j - 1]):
ops_rev.append(("delete", a[i - 1]))
i -= 1
else:
ops_rev.append(("insert", b[j - 1]))
j -= 1
ops_rev.reverse()
return ops_rev
+62
View File
@@ -0,0 +1,62 @@
import sys
sys.path.insert(0, "/root/.hermes/wizard-outputs/beam")
from lcs import lcs_length, lcs, lcs_diff
def test_empty():
assert lcs_length([], []) == 0
assert lcs([], [1,2]) == []
assert lcs([1,2], []) == []
def test_identical():
s = [1,2,3,4]
assert lcs_length(s, s) == 4
assert lcs(s, s) == s
def test_classic():
a = "ABCBDAB"
b = "BDCAB"
assert lcs_length(a, b) == 4
seq = lcs(a, b)
assert len(seq) == 4
# verify it's a subsequence of both
def subseq(x, y):
it = iter(y)
return all(c in it for c in x)
assert subseq(seq, a) and subseq(seq, b)
def test_no_common():
assert lcs_length("abc", "xyz") == 0
assert lcs("abc", "xyz") == []
def test_diff_roundtrip():
a = [1, 2, 3, 4, 5]
b = [2, 4, 6]
ops = lcs_diff(a, b)
# reconstruct b from a applying ops
out = []
for op, item in ops:
if op in ("keep", "insert"):
out.append(item)
# delete: skip
assert out == b
# deletes+keeps should cover a
covered = [item for op, item in ops if op in ("keep", "delete")]
assert covered == a
def test_strings():
assert lcs("kitten", "sitting") == list("ittn") or len(lcs("kitten","sitting"))==4
assert lcs_length("kitten", "sitting") == 4
def test_type_error():
try:
lcs_length(5, [1])
assert False, "expected TypeError"
except TypeError:
pass