"""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