beam: regex matching (LeetCode 10) recursive+DP with cross-check tests

This commit is contained in:
beam committed 2026-11-03 10:52:01 +00:00
1 parent 5a9d4a277e
commit bcaede7af3
2 files changed
+146

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
"""Regular expression matching supporting '.' and '*' (LeetCode 10).
'.' matches any single character; '*' matches zero or more of the
preceding element. The match must cover the *entire* input string.
Two implementations are provided:
- is_match(s, p): recursive backtracking with memoisation.
- is_match_dp(s, p): bottom-up DP table.
Both agree on all inputs; tests cross-check them.
"""
from __future__ import annotations
from functools import lru_cache
def _norm(p: str) -> str:
return p
def is_match(s: str, p: str) -> bool:
"""Recursive memoised matcher."""
if not isinstance(s, str) or not isinstance(p, str):
raise TypeError("s and p must be str")
# Precompute lengths.
n, m = len(s), len(p)
from functools import lru_cache
@lru_cache(maxsize=None)
def rec(i: int, j: int) -> bool:
# i position in s, j position in p
if j == m:
return i == n
# peek next char for '*'
first = (i < n) and (p[j] == s[i] or p[j] == '.')
if j + 1 < m and p[j + 1] == '*':
# zero occurrences OR one+ occurrences
return rec(i, j + 2) or (first and rec(i + 1, j))
if first:
return rec(i + 1, j + 1)
return False
return rec(0, 0)
def is_match_dp(s: str, p: str) -> bool:
if not isinstance(s, str) or not isinstance(p, str):
raise TypeError("s and p must be str")
n, m = len(s), len(p)
# dp[i][j] = s[:i] matches p[:j]
dp = [[False] * (m + 1) for _ in range(n + 1)]
dp[0][0] = True
# patterns like a*, a*b*, ... match empty string
for j in range(2, m + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2]
for i in range(1, n + 1):
for j in range(1, m + 1):
if p[j - 1] == '*':
# zero occurrences
dp[i][j] = dp[i][j - 2]
# one+ occurrences
if (p[j - 2] == s[i - 1] or p[j - 2] == '.') and dp[i - 1][j]:
dp[i][j] = True
elif p[j - 1] == '.' or p[j - 1] == s[i - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = False
return dp[n][m]
# cross-check alias
def match(s: str, p: str) -> bool:
return is_match(s, p)
+70
View File
@@ -0,0 +1,70 @@
import itertools
from regex_match import is_match, is_match_dp, match
CASES = [
("", "", True),
("", "a*", True),
("", ".", False),
("a", "a", True),
("a", ".", True),
("ab", ".*", True),
("aab", "c*a*b", True),
("mississippi", "mis*is*p*.", False),
("mississippi", "mis*is*ip*.", True),
("aa", "a", False),
("aa", "a*", True),
("ab", ".*c", False),
("aaa", "a*a", True),
("aaa", "ab*a*c*a", True),
("a", "ab*a", False),
("", "a*b*c*", True),
("abc", "a.c", True),
("abc", "a..", True),
("abcd", "d*", False),
]
def test_cases_recursive():
for s, p, exp in CASES:
assert is_match(s, p) is exp, (s, p, exp)
def test_cases_dp():
for s, p, exp in CASES:
assert is_match_dp(s, p) is exp, (s, p, exp)
def test_cross_check_random():
import random
rng = random.Random(42)
alphabet = "ab."
for _ in range(2000):
n = rng.randint(0, 6)
s = "".join(rng.choice("ab") for _ in range(n))
# build pattern with possible stars
m = rng.randint(0, 6)
toks = []
for _ in range(m):
c = rng.choice("ab.")
if rng.random() < 0.4:
toks.append(c + "*")
else:
toks.append(c)
p = "".join(toks)
r1 = is_match(s, p)
r2 = is_match_dp(s, p)
assert r1 == r2, (s, p, r1, r2)
def test_type_errors():
for f in (is_match, is_match_dp):
try:
f(123, "a")
assert False
except TypeError:
pass
def test_match_alias():
assert match("aa", "a*") is True