Skip to content
DSAPrep

    ↑↓ to move · Enter to open · Esc to close

    Hard2-D DP

    Regular Expression Matching

    Given an input string s and a pattern p, implement regular expression matching with support for . and * where . matches any single character, and * matches zero or more of the preceding element.

    Return a boolean indicating whether the matching covers the entire input string (not partial).

    Example 1

    Input: s = "aa", p = "a"

    Output: false

    Explanation: "a" does not match the entire string "aa".

    Example 2

    Input: s = "aa", p = "a*"

    Output: true

    Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

    Example 3

    Input: s = "ab", p = ".*"

    Output: true

    Explanation: ".*" means zero or more of any character.

    Constraints

    • 1 <= s.length <= 20
    • 1 <= p.length <= 20
    • s contains only lowercase English letters.
    • p contains only lowercase English letters, '.', and '*'.
    • It is guaranteed for each appearance of the character '*', there will be a previous valid character to match.
    View original on LeetCode ↗

    The character * never stands alone — it always modifies the character immediately before it in the pattern, so the pattern must always be consumed two tokens at a time whenever a * is next. That “look two ahead” quirk is what makes this DP’s recurrence a bit more intricate than a typical two-string comparison: a single decision (matching s[i] against p[j]) can require checking p[j+1] before knowing what to do.

    Brute Force Recursion

    Time O(2^(m+n))Space O(m+n)

    At each pair of positions (i, j): first check whether the next pattern token is *. If so, that * can be used zero times (skip both p[j] and p[j+1], retry from p[j+2]) or, when the current characters are compatible, one-or-more times (consume s[i], retry from the same p[j] since * can repeat). If the next token isn’t *, it’s a plain one-character match.

    class Solution:
    def isMatch(self, s: str, p: str) -> bool:
    def rec(i: int, j: int) -> bool:
    if j == len(p):
    return i == len(s)
    first_match = i < len(s) and p[j] in (s[i], '.')
    if j + 1 < len(p) and p[j + 1] == '*':
    zero_times = rec(i, j + 2)
    one_or_more = first_match and rec(i + 1, j)
    return zero_times or one_or_more
    return first_match and rec(i + 1, j + 1)
    return rec(0, 0)

    Each * can branch into “use it” or “don’t”, and the same (i, j) state is reached through many different repeat counts — exponential blow-up.

    2-D DP Table (Bottom-Up)

    OptimalTime O(m·n)Space O(m·n)

    dp[i][j] is true if s[0:i] matches p[0:j]. dp[0][0] is trivially true (empty matches empty). Row 0 (s empty) can still be true for patterns like a*b* — zero or more of everything — so it’s initialized by checking whether trailing x* pairs can all collapse to nothing. For the general cell: if p[j-1] is a plain character or ., it must match s[i-1] and the rest is dp[i-1][j-1]. If p[j-1] is *, either drop the x* pair entirely (dp[i][j-2]), or — if s[i-1] matches the character * is repeating — also allow dp[i-1][j] (use one more repetition, keeping the same pattern position).

    class Solution:
    def isMatch(self, s: str, p: str) -> bool:
    m, n = len(s), len(p)
    dp = [[False] * (n + 1) for _ in range(m + 1)]
    dp[0][0] = True
    for j in range(1, n + 1):
    if p[j - 1] == '*':
    dp[0][j] = dp[0][j - 2]
    for i in range(1, m + 1):
    for j in range(1, n + 1):
    if p[j - 1] in (s[i - 1], '.'):
    dp[i][j] = dp[i - 1][j - 1]
    elif p[j - 1] == '*':
    dp[i][j] = dp[i][j - 2]
    if p[j - 2] in (s[i - 1], '.'):
    dp[i][j] = dp[i][j] or dp[i - 1][j]
    else:
    dp[i][j] = False
    return dp[m][n]

    Watch the table fill for statement example s = "aa", p = "a*" — the indigo cell is the one being decided, the amber cells are the already-computed inputs its rule reads (diagonal for a plain match, two cells back and straight up for a *), and emerald cells are the True answers that have accumulated:

    s↓ p→
    a
    a*
    T
    ·
    ·
    a
    ·
    ·
    ·
    aa
    ·
    ·
    ·
    1 / 9
    currentresultcomparingseen

    Seed the corner: dp[0][0] = T — the empty string matches the empty pattern. Every other cell answers one pair of prefixes, and each builds on this corner.

    The emerald chain down the a* column is the whole story: a* matches zero as (dp[0][2]), then one (dp[1][2]), then two (dp[2][2]) — every extend-branch read of the star rule, dp[i][j] = dp[i-1][j], consumes one more character of s while the pattern position never moves. The two-cells-back drop branch (dp[i][j-2]) is what zero repetitions means, and the seed row showed it firing in its purest form.

    Two-star-group cases exercise the same rules in shapes that depend on the pattern, so it helps to read one aloud — working through s = "aab", p = "c*a*b" (two different * groups plus a trailing literal) in words:

    • p = "c*a*b" against empty s: c* can vanish (0 uses of c), and a* can vanish too, leaving just b — which doesn’t match empty s. So dp[0][j] is only true through the a* prefix, i.e. dp[0][2] (c* alone) and dp[0][4] (c*a*) are true, not dp[0][5].
    • Matching s[0]='a' against p: the c* group matches zero cs (since s has no leading c), then a* starts consuming as.
    • a* can consume both as in "aab" (one or more repetitions extend dp[i-1][j] forward each time s[i-1] == 'a'), landing on dp[2][4] = True — two as consumed, c*a* fully matched.
    • Finally b matches the trailing b in s, giving dp[3][5] = True — confirmed by running the code above: isMatch("aab", "c*a*b") returns True.

    Complexity: each of the (m+1)·(n+1) cells does O(1) additional work (checking two pattern characters) → O(m·n) time, O(m·n) space.

    Top-Down Memoization

    Time O(m·n)Space O(m·n)

    The recursive formulation from the brute-force solution is arguably easier to read than the bottom-up table here, because the “look at p[j+1]” logic reads naturally as “peek ahead” rather than as index arithmetic on j-2. Adding a memo cache turns it from exponential into polynomial without changing its structure at all.

    from functools import cache
    class Solution:
    def isMatch(self, s: str, p: str) -> bool:
    @cache
    def rec(i: int, j: int) -> bool:
    if j == len(p):
    return i == len(s)
    first_match = i < len(s) and p[j] in (s[i], '.')
    if j + 1 < len(p) and p[j + 1] == '*':
    return rec(i, j + 2) or (first_match and rec(i + 1, j))
    return first_match and rec(i + 1, j + 1)
    return rec(0, 0)

    Complexity: at most (m+1)·(n+1) distinct (i, j) states are ever computed, each doing O(1) work — O(m·n) time, O(m·n) space for the cache plus recursion stack.

    More 2-D DP practice