From bcaede7af3d907d57310d55a112a903507022c6a Mon Sep 17 00:00:00 2001 From: beam Date: Tue, 3 Nov 2026 10:52:01 +0000 Subject: [PATCH] beam: regex matching (LeetCode 10) recursive+DP with cross-check tests --- wizard-work/beam/regex_match.py | 76 ++++++++++++++++++++++++++++ wizard-work/beam/test_regex_match.py | 70 +++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 wizard-work/beam/regex_match.py create mode 100644 wizard-work/beam/test_regex_match.py diff --git a/wizard-work/beam/regex_match.py b/wizard-work/beam/regex_match.py new file mode 100644 index 0000000..b4b16d4 --- /dev/null +++ b/wizard-work/beam/regex_match.py @@ -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) \ No newline at end of file diff --git a/wizard-work/beam/test_regex_match.py b/wizard-work/beam/test_regex_match.py new file mode 100644 index 0000000..c06a34f --- /dev/null +++ b/wizard-work/beam/test_regex_match.py @@ -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 \ No newline at end of file