27. Match a Routing Pattern
Problem
Fin's agent-authored routing rules use a simplified wildcard pattern language to
send incoming queries to specialized handlers: . matches any single
character, and * matches zero or more occurrences of the character
immediately preceding it in the pattern.
Given an incoming query string s and a routing pattern
pattern, determine whether the pattern matches the ENTIRE query
string, not just a substring of it.
Source: src/27_matches_routing_pattern.py
def is_match(s: str, pattern: str) -> bool: ...
is_match("aa", "a") # -> False ("a" only matches one char, s has two)
is_match("aa", "a*") # -> True ("a*" matches zero or more "a"s)
is_match("ab", ".*") # -> True (".*" matches anything)
is_match("mississippi", "mis*is*p*.") # -> False
Step-by-Step Approach
- Let
n = len(s),m = len(pattern). Build a(n+1) x (m+1)boolean DP table wheredp[i][j]means "s[i:]matchespattern[j:]" — i.e. matching the remaining suffixes. - Base case:
dp[n][m] = True, since two empty suffixes match. - Fill the table backward —
ifromndown to 0,jfromm-1down to 0 — so thatdp[i][j]can rely on already-computed cells to its right/below. - At each cell, first check whether the current characters line up:
first_match = i < n and pattern[j] in (s[i], "."). - If the pattern character after
jis a*(pattern[j+1] == "*"), there are two ways to satisfy it: use zero occurrences and skip bothpattern[j]and the*(dp[i][j+2]), or, if the current characters match, consume one character ofsand stay on the same pattern position (first_match and dp[i+1][j]).dp[i][j]isTrueif either option works. - Otherwise (no following
*), the cell is only true if the current characters match AND the rest of the suffixes match:dp[i][j] = first_match and dp[i+1][j+1]. - The answer is
dp[0][0]— do the full strings match from the start?
The key insight is that * makes the "consume one character vs. skip
this pattern token" decision reusable at every position, so filling the table from
the end backward lets each cell look up already-solved smaller subproblems instead
of re-deciding from scratch. Time and space are both O(n * m).
Reference solution
def is_match(s: str, pattern: str) -> bool:
# dp[i][j] = whether s[i:] matches pattern[j:], filled backward, O(len(s)*len(pattern)) time/space
n, m = len(s), len(pattern)
# per-row list, avoids aliasing bug
dp = [[False] * (m + 1) for _ in range(n + 1)]
dp[n][m] = True
# count down to 0 inclusive
for i in range(n, -1, -1):
# count down to 0 inclusive
for j in range(m - 1, -1, -1):
# bounds-safe char-or-dot match
first_match = i < n and pattern[j] in (s[i], ".")
# lookahead: * applies to pattern[j]
if j + 1 < m and pattern[j + 1] == "*":
# skip char* (0 reps) or match one more
dp[i][j] = dp[i][j + 2] or (first_match and dp[i + 1][j])
else:
dp[i][j] = first_match and dp[i + 1][j + 1]
return dp[0][0]
Key Functions & Tricks
[[False] * (m + 1) for _ in range(n + 1)]— independent per-row list, sized(n+1) x (m+1).dp[i][j]— true whens[i:]matchespattern[j:], filled backward.range(n, -1, -1)— negative-step range counts down to 0 inclusive.first_match = i < n and pattern[j] in (s[i], ".")— bounds-safe char-or-dot match check.*-handling — lookaheadpattern[j+1] == "*"combines zero-occurrence skip (dp[i][j+2]) with one-more-repeat (dp[i+1][j]) viaor.
How to Recognize This Pattern
Signals: full-string matching against a pattern with wildcards or repetition
operators (., *, ?) is a strong tell for
a 2-D DP over positions in both strings, especially when a greedy or naive
recursive approach would need to backtrack. If a straightforward "try consuming
vs. skipping" recursion has overlapping subproblems, memoize it into a table.
Variations: implement ? (matches any single character, no repetition)
alongside *, which is a simpler wildcard-matching DP; or extend
* to also support + (one or more) with a small tweak to
the transition.
Common pitfall: treating * as applying to the character at position
j itself rather than the character immediately before it — the
lookahead check must be pattern[j+1] == "*", not
pattern[j] == "*", and cells must be filled in the right order
(backward here) so dependencies are already computed.