diff --git a/problems/0010-regular-expression-matching/analysis.md b/problems/0010-regular-expression-matching/analysis.md new file mode 100644 index 0000000..fb4d025 --- /dev/null +++ b/problems/0010-regular-expression-matching/analysis.md @@ -0,0 +1,68 @@ +# 0010. Regular Expression Matching + +[LeetCode Link](https://leetcode.com/problems/regular-expression-matching/) + +Difficulty: Hard +Topics: String, Dynamic Programming, Recursion +Acceptance Rate: 30.3% + +## Hints + +### Hint 1 + +Think about breaking down the problem into smaller subproblems. When you're comparing a string with a pattern, you're essentially making decisions character by character. What information do you need to know about the rest of the string and pattern after making a decision about the current characters? + +### Hint 2 + +The '*' character is the tricky part - it can match zero or more of the preceding character. This means when you encounter a pattern like "a*", you have two choices: either use the star (match one 'a' and keep the pattern "a*" available), or skip the star pattern entirely (match zero 'a's). Consider using dynamic programming or recursion with memoization to avoid recomputing the same subproblems. + +### Hint 3 + +Define your state as dp[i][j], representing whether s[0...i-1] matches p[0...j-1]. The key insight is handling the star case: if p[j-1] is '*', then dp[i][j] is true if either: +1. We skip the pattern (dp[i][j-2] is true - matching zero occurrences) +2. The preceding character matches (s[i-1] matches p[j-2]) AND dp[i-1][j] is true (we consume one character from s and keep trying to match more with the same pattern) + +## Approach + +We'll use dynamic programming with a 2D table where dp[i][j] represents whether the first i characters of string s match the first j characters of pattern p. + +**Base Cases:** +- dp[0][0] = true (empty string matches empty pattern) +- dp[0][j] can be true only if the pattern can match empty string (patterns like "a*", "a*b*", etc.) + +**State Transitions:** + +For each position (i, j): + +1. **If p[j-1] is not '*'**: + - dp[i][j] = dp[i-1][j-1] AND (s[i-1] == p[j-1] OR p[j-1] == '.') + - This means the current characters must match, and the previous substrings must match. + +2. **If p[j-1] is '*'**: + - We have two options: + - **Zero occurrences**: dp[i][j] = dp[i][j-2] (skip the character and '*') + - **One or more occurrences**: If s[i-1] matches p[j-2] (the character before '*'), then dp[i][j] = dp[i-1][j] (consume one character from s and keep the pattern) + - dp[i][j] = dp[i][j-2] OR (matches AND dp[i-1][j]) + - Where matches = (s[i-1] == p[j-2] OR p[j-2] == '.') + +**Example Walkthrough:** +For s = "aa" and p = "a*": +- dp[0][0] = true +- dp[0][2] = true (because "a*" can match empty string) +- dp[1][2] = true (first 'a' matches "a*") +- dp[2][2] = true (both 'a's match "a*") + +## Complexity Analysis + +Time Complexity: O(m * n), where m is the length of the string s and n is the length of the pattern p. We fill a 2D table of size (m+1) x (n+1). + +Space Complexity: O(m * n) for the DP table. This can be optimized to O(n) by using only two rows, but the standard approach uses O(m * n). + +## Edge Cases + +1. **Empty string with star patterns**: Patterns like "a*b*c*" should match an empty string. +2. **Single dot**: "." should match any single character. +3. **Dot with star**: ".*" should match any string (zero or more of any character). +4. **Multiple consecutive star patterns**: "a*a*" is a valid pattern that needs proper handling. +5. **Pattern longer than string**: A pattern like "a*b*c*" can match shorter strings or even empty strings. +6. **Exact match without wildcards**: Basic string comparison when pattern has no special characters. diff --git a/problems/0010-regular-expression-matching/solution.go b/problems/0010-regular-expression-matching/solution.go new file mode 100644 index 0000000..8624d7d --- /dev/null +++ b/problems/0010-regular-expression-matching/solution.go @@ -0,0 +1,52 @@ +package main + +// Dynamic Programming approach: +// dp[i][j] represents whether s[0...i-1] matches p[0...j-1] +// For each position, we check: +// 1. If current pattern char is not '*', it must match current string char (or be '.') +// 2. If current pattern char is '*', we can either skip it (zero occurrences) or use it (one or more occurrences) + +func isMatch(s string, p string) bool { + m, n := len(s), len(p) + + // dp[i][j] = true if s[0...i-1] matches p[0...j-1] + dp := make([][]bool, m+1) + for i := range dp { + dp[i] = make([]bool, n+1) + } + + // Base case: empty string matches empty pattern + dp[0][0] = true + + // Initialize first row: empty string with patterns like a*, a*b*, etc. + for j := 2; j <= n; j++ { + if p[j-1] == '*' { + dp[0][j] = dp[0][j-2] + } + } + + // Fill the DP table + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if p[j-1] == '*' { + // Star can match zero occurrences (skip pattern char and star) + dp[i][j] = dp[i][j-2] + + // Or star can match one or more occurrences + // Check if current char matches the char before '*' + charMatch := s[i-1] == p[j-2] || p[j-2] == '.' + if charMatch { + dp[i][j] = dp[i][j] || dp[i-1][j] + } + } else { + // Current pattern char must match current string char (or be '.') + charMatch := s[i-1] == p[j-1] || p[j-1] == '.' + if charMatch { + dp[i][j] = dp[i-1][j-1] + } + } + } + } + + return dp[m][n] +} diff --git a/problems/0010-regular-expression-matching/solution_test.go b/problems/0010-regular-expression-matching/solution_test.go new file mode 100644 index 0000000..de78d00 --- /dev/null +++ b/problems/0010-regular-expression-matching/solution_test.go @@ -0,0 +1,94 @@ +package main + +import "testing" + +func TestIsMatch(t *testing.T) { + tests := []struct { + name string + s string + p string + expected bool + }{ + { + name: "example 1: pattern too short", + s: "aa", + p: "a", + expected: false, + }, + { + name: "example 2: star matches multiple chars", + s: "aa", + p: "a*", + expected: true, + }, + { + name: "example 3: dot star matches everything", + s: "ab", + p: ".*", + expected: true, + }, + { + name: "edge case: empty string with star pattern", + s: "", + p: "a*", + expected: true, + }, + { + name: "edge case: empty string with multiple star patterns", + s: "", + p: "a*b*c*", + expected: true, + }, + { + name: "edge case: single dot matches single char", + s: "a", + p: ".", + expected: true, + }, + { + name: "edge case: dot star at beginning", + s: "aab", + p: "c*a*b", + expected: true, + }, + { + name: "complex pattern with multiple stars", + s: "mississippi", + p: "mis*is*p*.", + expected: false, + }, + { + name: "complex pattern matching", + s: "mississippi", + p: "mis*is*ip*.", + expected: true, + }, + { + name: "star with no preceding match", + s: "ab", + p: ".*c", + expected: false, + }, + { + name: "exact match without wildcards", + s: "abc", + p: "abc", + expected: true, + }, + { + name: "pattern longer with stars", + s: "a", + p: "ab*", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isMatch(tt.s, tt.p) + if result != tt.expected { + t.Errorf("isMatch(%q, %q) = %v, want %v", tt.s, tt.p, result, tt.expected) + } + }) + } +}