Wildcard Pattern Matching
Given a text string and a pattern that may contain '?' for any single character and '*' for any sequence of characters, including none, decide whether the pattern matches the entire text.
Do this lesson first: longest common subsequenceExample input
pattern = "a*b?c", text = "abycbxcbzc"
Expected output
1 (the pattern matches)
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
Row 0 and column 0 are given rather than computed. dp[0][0] = 1: the empty pattern matches the empty text. dp[0][j] = 0 for j from 1 up: the empty pattern takes no letters, so a text with a letter in it is out of reach. Column 0 is the pattern against the empty text, and a star is the one character that can match nothing, so dp[i][0] = 1 exactly when the pattern's first i characters are all stars. That is a closed form of the pattern alone, not a recurrence, and the snippet writes it that way. On a*b?c rows 1 to 5 of column 0 are 0, row 2's a* included, because the a needs a letter. At the drive size that is 16 bases: 11 across row 0 for columns 0 to 10, and 5 more down column 0 for rows 1 to 5. A star cell reads the cell above and the cell to its left in its own row; a letter or ? cell reads the diagonal, up and to the left. A row-major sweep, left to right within each row, has finished each of those before it is needed, and the left-to-right part is what the star depends on: its left neighbour was written one step earlier in the same row.
Cols →
Rows ↓pattern: a*b?c
Cols →text: the first n letters of abycbxcbzca
- 1
function isMatch(p, t) { - 2
const m = p.length, n = t.length; - 3
const allStars = (i) => [...p.slice(0, i)].every((c) => c === "*"); // true at i = 0 - 4
const dp = Array.from({ length: m + 1 }, (_, i) => - 5
Array.from({ length: n + 1 }, (_, j) => (j === 0 && allStars(i) ? 1 : 0))); - 6
for (let i = 1; i <= m; i++) { - 7
for (let j = 1; j <= n; j++) { - 8
const star = p[i - 1] === "*"; - 9
const ok = p[i - 1] === "?" || p[i - 1] === t[j - 1] ? 1 : 0; - 10
dp[i][j] = star ? Math.max(dp[i - 1][j], dp[i][j - 1]) : Math.min(ok, dp[i - 1][j - 1]); - 11
} - 12
} - 13
return dp[m][n]; - 14
}
The code, the trap, the variations
- 1
function isMatch(p, t) { - 2
const m = p.length, n = t.length; - 3
const allStars = (i) => [...p.slice(0, i)].every((c) => c === "*"); // true at i = 0 - 4
const dp = Array.from({ length: m + 1 }, (_, i) => - 5
Array.from({ length: n + 1 }, (_, j) => (j === 0 && allStars(i) ? 1 : 0))); - 6
for (let i = 1; i <= m; i++) { - 7
for (let j = 1; j <= n; j++) { - 8
const star = p[i - 1] === "*"; - 9
const ok = p[i - 1] === "?" || p[i - 1] === t[j - 1] ? 1 : 0; - 10
dp[i][j] = star ? Math.max(dp[i - 1][j], dp[i][j - 1]) : Math.min(ok, dp[i - 1][j - 1]); - 11
} - 12
} - 13
return dp[m][n]; - 14
}
Where people go wrong
Letting the star read the diagonal instead of its left neighbour: max(dp[i - 1][j], dp[i - 1][j - 1]). It looks like the letter and ? rows, which read the diagonal too, but the diagonal moves the pattern on past the star, so the star can take nothing or exactly one letter and is then spent; it has turned into a ? that may be skipped. On this input row 2 then holds 1 at columns 1 and 2 and 0 from column 3 on, where the correct row 2 is 1 at each column from 1 up. The slider gives 1, 0, 0, 0, 0, 0, 0, 0 instead of 1, 0, 0, 1, 0, 0, 1, 0: right at length 4, where the star takes nothing, and wrong at length 7 and at the drive size 10, where it has to take three letters and six. The left read stays in the star's own row so that the star can keep taking letters.
Add a + that matches one or more letters, and use it in place of the star: a+b?c.
The recurrence gains one rule, for a + row, and the letter, ? and * rules stay. A + cannot match nothing, so the cell above drops out. It takes the text's last letter either as the first letter of its run, which moves the pattern on past it and reads the diagonal, or as one more letter in a run already started, which keeps it in play and reads the cell to the left in its own row: dp[i][j] = max(dp[i - 1][j - 1], dp[i][j - 1]). A + is a ? followed by a *, and this is those two rows folded into one. Among the bases, row 0 stays as it is and column 0 keeps its closed form, all stars; a + is not a star, so a prefix that contains one gets 0. With a+b?c the slider gives 0, 0, 0, 1, 0, 0, 1, 0: length 4 drops to 0, because abyc matched a*b?c with the star taking nothing, and a + has to take at least one letter, which leaves b?c two letters for its three.
Match letters case-insensitively.
Only ok changes: compare p[i - 1] and t[j - 1] after lower-casing both, so A against a gives ok = 1. The star rule, the ? rule, the bases and the sweep stay as they are. This page's pattern and text are already lowercase, so its answers do not move. With the text in capitals, ABYCBXCBZCA, the case-sensitive table answers 0 at all eight lengths, since row 1's a meets an A in column 1 and fails, and the case-insensitive one gives back 1, 0, 0, 1, 0, 0, 1, 0.