Topics › Dynamic Programming › DP on Strings
Wildcard matching
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a string s and a pattern p. The pattern may contain ordinary letters and two wildcards:
?matches exactly one character, whatever it is.*matches any sequence of characters, including the empty sequence.
Return true if the pattern matches the whole of s, not just a part of it.
Constraints (LeetCode 44): 0 ≤ |s|, |p| ≤ 2000, s has lowercase letters, p has lowercase letters, ? and *.
Example 1
s = "adceb", p = "*a*b"
answer = true // first * matches "", a matches a, second * matches "dce", b matches b
Example 2
s = "acdcb", p = "a*c?b"
answer = false // after a, the pattern needs ...c?b at the end, but s ends in "dcb"
Example 3
s = "aa", p = "a"
answer = false // the pattern covers only the first character
These are file-name globs, not regular expressions. In a regex, * repeats the previous token; here * stands on its own and matches anything.
The recurrence
Write f(i, j) for “does the string prefix s[0..i) match the pattern prefix p[0..j)?”. Base cases:
f(0, 0) = true: an empty pattern matches an empty string.f(i, 0) = falsefori > 0: an empty pattern cannot match characters.f(0, j)is true exactly whenp[0..j)is all stars, because only*can match nothing.
For i, j > 0, look at the last pattern character pc = p[j - 1]:
pcis?or equalss[i - 1]. It consumes exactly that character, sof(i, j) = f(i - 1, j - 1).pcis*. It matches some numberk ≥ 0of trailing characters ofs. Instead of looping overk, split into two cases:k = 0: the star matches nothing and disappears, givingf(i, j - 1).k ≥ 1: the star swallowss[i - 1]and stays in the pattern, so it can swallow more. That isf(i - 1, j).
So
f(i, j) = f(i, j - 1) || f(i - 1, j).- Otherwise
pcis a different letter, andf(i, j) = false.
The star case is the heart of the problem. Unrolling f(i - 1, j) again and again gives f(i - 1, j - 1), f(i - 2, j - 1), and so on, which is exactly “the star matches 1, 2, … characters”. Keeping the star in place turns an O(n) loop per state into two constant-time lookups.
Approach 1: Recursion
Intuition. Write the recurrence directly. The order of the base cases matters: test j == 0 first so that f(0, 0) is true, then handle i == 0 by checking that the remaining pattern is all stars.
Algorithm.
- If
j == 0, returni == 0. - If
i == 0, return whetherp[0..j)contains only*. - Apply the three cases above.
static boolean recursive(String s, String p) {
return solve(s.length(), p.length(), s, p);
}
/** Does the prefix s[0..i) match the pattern prefix p[0..j)? */
static boolean solve(int i, int j, String s, String p) {
if (j == 0) return i == 0; // empty pattern matches only the empty string
if (i == 0) return allStars(p, j); // empty string: the pattern must be all '*'
char pc = p.charAt(j - 1);
if (pc == '?' || pc == s.charAt(i - 1)) return solve(i - 1, j - 1, s, p);
if (pc == '*') return solve(i, j - 1, s, p) // the star matches nothing
|| solve(i - 1, j, s, p); // the star swallows s[i - 1] and may continue
return false; // two different letters
}
/** True when p[0..j) consists only of '*'. */
static boolean allStars(String p, int j) {
for (int k = 0; k < j; k++) if (p.charAt(k) != '*') return false;
return true;
}
Complexity. Time exponential: every star can branch two ways, and a pattern like "*a*a*a*b" against a long string of as explores a huge number of ways to distribute the characters among the stars. Space O(n + m) for the recursion stack.
Approach 2: Memoization
Intuition. The result depends only on (i, j), so there are (n + 1)(m + 1) distinct subproblems. The exponential blow-up comes from reaching the same pair through different star splits, so caching each answer fixes it. A boolean[][] cannot tell “not computed” from false, so the table stores 0 for unknown, 1 for false and 2 for true.
Algorithm.
- Create
int dp[n + 1][m + 1], all zeros. - Run the recursion, returning the cached value when
dp[i][j] != 0, and storingok ? 2 : 1after computing.
static boolean memoization(String s, String p) {
int[][] dp = new int[s.length() + 1][p.length() + 1];
return memo(s.length(), p.length(), s, p, dp);
}
static boolean memo(int i, int j, String s, String p, int[][] dp) {
if (j == 0) return i == 0;
if (i == 0) return allStars(p, j);
if (dp[i][j] != 0) return dp[i][j] == 2;
char pc = p.charAt(j - 1);
boolean ok;
if (pc == '?' || pc == s.charAt(i - 1)) ok = memo(i - 1, j - 1, s, p, dp);
else if (pc == '*') ok = memo(i, j - 1, s, p, dp) || memo(i - 1, j, s, p, dp);
else ok = false;
dp[i][j] = ok ? 2 : 1;
return ok;
}
Complexity. Time O(n · m) for the states. The all-star check at i == 0 is not cached, but it is reached at most twice per cell of row 1, adding at most O(m^2). Space O(n · m) plus O(n + m) stack.
Approach 3: Tabulation
Intuition. Cell (i, j) reads the diagonal (i - 1, j - 1), the cell above (i - 1, j) and the cell to the left (i, j - 1). Filling rows top to bottom and each row left to right makes all three ready. Row 0 is filled with a running “all stars so far” flag, dp[0][j] = dp[0][j - 1] && p[j - 1] == '*', which also removes the O(m^2) base-case cost of the memoized version. Column 0 below row 0 stays false.
Algorithm.
dp[0][0] = true; fill row 0 with the running star flag.- For
i = 1 .. n,j = 1 .. m: diagonal for?or a matching letter; left OR above for*; otherwisefalse. - Return
dp[n][m].
static boolean tabulation(String s, String p) {
int n = s.length(), m = p.length();
boolean[][] dp = new boolean[n + 1][m + 1];
dp[0][0] = true;
for (int j = 1; j <= m; j++) dp[0][j] = dp[0][j - 1] && p.charAt(j - 1) == '*';
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
char pc = p.charAt(j - 1);
if (pc == '?' || pc == s.charAt(i - 1)) dp[i][j] = dp[i - 1][j - 1];
else if (pc == '*') dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
else dp[i][j] = false;
}
}
return dp[n][m];
}
Dry run on Example 1 (s = "adceb" down the side, p = "*a*b" across, T = true):
| ”” | * | a | * | b | |
|---|---|---|---|---|---|
| ”” | T | T | F | F | F |
| a | F | T | T | T | F |
| d | F | T | F | T | F |
| c | F | T | F | T | F |
| e | F | T | F | T | F |
| b | F | T | F | T | T |
The first * column is all true: a leading star can absorb any prefix. The bold T in row a is the letter match a = a, inherited diagonally from (0, 1). The second * column then stays true all the way down, because each cell reads the true value above it (the star swallowing one more character). Finally b matches b and takes the diagonal value from row e, column *.
Complexity. Time O(n · m), space O(n · m).
Approach 4: Space optimized
Intuition. Each row reads only the previous row and itself, so two arrays suffice. Build row 0 into prev, then for each character of s fill cur and swap. Because the arrays are reused, cur[0] must be reset to false at the start of every row; after the first swap cur is the old row 0, whose cur[0] is true.
Algorithm.
- Fill
prevas row 0. - For each
i = 1 .. n: setcur[0] = false, fillcur[1..m], swap. - Return
prev[m].
static boolean spaceOptimized(String s, String p) {
int n = s.length(), m = p.length();
boolean[] prev = new boolean[m + 1], cur = new boolean[m + 1];
prev[0] = true;
for (int j = 1; j <= m; j++) prev[j] = prev[j - 1] && p.charAt(j - 1) == '*';
for (int i = 1; i <= n; i++) {
cur[0] = false; // a non-empty string never matches an empty pattern
for (int j = 1; j <= m; j++) {
char pc = p.charAt(j - 1);
if (pc == '?' || pc == s.charAt(i - 1)) cur[j] = prev[j - 1];
else if (pc == '*') cur[j] = cur[j - 1] || prev[j];
else cur[j] = false;
}
boolean[] tmp = prev; prev = cur; cur = tmp;
}
return prev[m];
}
Complexity. Time O(n · m), space O(m).
Approach 5: Greedy with backtracking to the last star
Intuition. Stars split the pattern into star-free segments, like a and b in *a*b. Match each segment at the earliest place it fits in s. Matching earlier never hurts, because it leaves more of s for what follows, and the next star can always absorb the gap. So when a mismatch happens we never need to revisit an older star. We only let the most recent star swallow one more character and retry the segment after it. If there is no star to fall back on, the match fails.
Algorithm.
- Pointers
iinsandjinp.star = -1is the position of the last*seen,markis where insthat star’s match ends. - While
i < |s|:- If
p[j]is?or equalss[i], advance both. - Else if
p[j]is*, recordstar = j,mark = i, and advancejonly (the star matches nothing for now). - Else if a star exists, backtrack:
j = star + 1,mark++,i = mark(the star swallows one more character). - Else return
false.
- If
- Skip trailing stars in
p; return whetherjreached the end ofp.
static boolean greedy(String s, String p) {
int i = 0, j = 0, star = -1, mark = 0; // star = index of the last '*', mark = where it started in s
while (i < s.length()) {
if (j < p.length() && (p.charAt(j) == '?' || p.charAt(j) == s.charAt(i))) {
i++;
j++;
} else if (j < p.length() && p.charAt(j) == '*') {
star = j++; // first let the star match nothing
mark = i;
} else if (star != -1) {
j = star + 1; // stuck: let the last star swallow one more character
i = ++mark;
} else {
return false;
}
}
while (j < p.length() && p.charAt(j) == '*') j++; // trailing stars can match the empty rest
return j == p.length();
}
Dry run on Example 1 (s = "adceb", p = "*a*b"):
| step | i | j | action |
|---|---|---|---|
| 1 | 0 | 0 | *: star = 0, mark = 0, j = 1 |
| 2 | 0 | 1 | a = a: i = 1, j = 2 |
| 3 | 1 | 2 | *: star = 2, mark = 1, j = 3 |
| 4 | 1 | 3 | b ≠ d: backtrack, mark = 2, i = 2, j = 3 |
| 5 | 2 | 3 | b ≠ c: backtrack, mark = 3, i = 3, j = 3 |
| 6 | 3 | 3 | b ≠ e: backtrack, mark = 4, i = 4, j = 3 |
| 7 | 4 | 3 | b = b: i = 5, j = 4 |
s is used up, there are no trailing stars to skip, and j = 4 = |p|, so the answer is true. The first star was never revisited once the second star appeared.
Complexity. Time O(n · m) in the worst case: each backtrack advances mark by one and may rescan a segment of length up to m. On typical inputs it is close to linear. Space O(1). This is the approach to use when memory matters; the DP versions are easier to adapt to variants such as counting matches.
Edge cases and pitfalls
- Empty string.
("", "***")is true,("", "?")is false:?needs exactly one character. - Empty pattern.
("", "")is true,("a", "")is false. Checkj == 0beforei == 0. - Full match.
("aa", "a")is false. The answer is the bottom-right cell, not “any true cell in the last column”. - Row 0. Only a leading run of stars can match the empty string. A single non-star breaks the run for every later column.
- Stale
cur[0]. Reset it in every row of the two-array version. - Trailing stars in the greedy. After
sis consumed,pmay still have stars; skip them before checkingj == |p|. - Not a regex. Do not treat
*as “repeat the previous character”. - Testing. The Java file cross-checks all five methods on 400 seeded random cases over
{a, b, ?, *}against Java’s own regex engine, with?translated to.and*to.*.
Related problems
- Edit distance uses the same three neighbours (left, above, diagonal) with costs instead of booleans.
- Distinct subsequences counts ways to produce one string from another with the same prefix-length table.
- Longest common subsequence is the base pattern for “compare the last characters of two prefixes”.
- Grid Unique Paths : DP on Grids (DP8) is the plainest grid DP; here a star moves right or down in the table and a letter match moves diagonally.