beam: regex matching (LeetCode 10) recursive+DP with cross-check tests
This commit is contained in:
1 parent
5a9d4a277e
commit
bcaede7af3
2 files changed
+146
No files matched your search
@@ -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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user