(Version 1.0)
这题在LeetCode的标签有Dynamic Programming,但是实际上的能通过OJ的解法好像不应该被称为DP,感觉这个tag貌似比较有欺骗性。一家之见。
由Regular Expression Matching的解法而来的DP解法探究
这题在LeetCode中的标签是Dynamic Programming, Backtracking, Greedy和String,做题之前目测解法要比之前的Regular Expression Matching更精巧,不会是那么straightforward的DP。不过受到Regular Expression Matching的启发,打算先用类似(甚至可以说是相同)的解法解一下,代码如下:
1 public class Solution { 2 public boolean isMatch(String s, String p) { 3 boolean[][] match = new boolean[s.length() + 1][p.length() + 1]; 4 match[0][0] = true; 5 for (int i = 0; i <= s.length(); i++) { 6 for (int j = 1; j <= p.length(); j++) { 7 if (i != 0) { 8 char c = p.charAt(j - 1); 9 if (c != '*') { 10 match[i][j] = match[i - 1][j - 1] && (c == '?' || c == s.charAt(i - 1)); 11 } else { 12 match[i][j] = match[i][j - 1] || match[i - 1][j - 1] || match[i - 1][j]; 13 } 14 } else { 15 if (p.charAt(j - 1) == '*') { 16 match[0][j] = true; 17 } else { 18 break; 19 } 20 } 21 } 22 } 23 return match[s.length()][p.length()]; 24 } 25 }
这个答案不出所料地得到了Memory Limit Exceeded的结果,那么试一试不开二维数组,而只开一维数组行不行呢?代码如下:
1 public class Solution { 2 public boolean isMatch(String s, String p) { 3 boolean[][] match = new boolean[2][p.length() + 1]; 4 match[0][0] = true; 5 for (int j = 0; j < p.length(); j++) { // initialize first row of match 6 if (p.charAt(j) == '*') { 7 match[0][j] = true; 8 } else { 9 break; 10 } 11 } 12 for (int i = 1; i <= s.length(); i++) { 13 for (int j = 1; j <= p.length(); j++) { 14 char c = p.charAt(j - 1); 15 if (c != '*') { 16 match[1][j] = match[0][j - 1] && (c == '?' || c == s.charAt(i - 1)); 17 } else { 18 match[1][j] = match[1][j - 1] || match[0][j - 1] || match[0][j]; 19 } 20 } 21 System.arraycopy(match[1], 0, match[0], 0, match[0].length); 22 } 23 return match[1][p.length()]; 24 } 25 }
依然没有通过,得到的是Time Limit Exceeded。说明这题的思路需要不同于Regular Expression Matching那种近乎brutal force的DP。
通过了OJ的two pointers解法
仔细思考一下这题的核心所在,发现问题和Regular Expression Matching的核心类似,其实还是'*'到底要match多少个s中的char。'*'可以match 0/1/多个,所以我们需要在遇到'*'时依然要对不同情形进行试探,而在试探失败之后怎么reset到之前的状态就是问题的核心了。这题使用额外的空间会Memory Limit Exceeded,自然会想到那我们只能用two pointer了,于是问题进一步变成了:如果试探失败,怎么reset这两个pointer到应该去的位置,或者说哪里才是它们应该去的位置。如果我们从匹配0个s中的char开始试探,那么“试探失败”其实意味着我们用'*'来匹配的s中的char匹配少了,需要用一个'*'来匹配更多s中的char,所以对于s的指针,应该reset到的位置应该是上一个被'*'匹配的char后面的第一个char,而对于p的指针,因为我们永远从匹配0个开始,所以依然是reset到'*'之后的第一个char,代码如下:
1 public boolean isMatch(String s, String p) { 2 int asterisk = -1; 3 int lastMatch = -1; 4 int i = 0; 5 int j = 0; 6 while (i < s.length()) { // this loop guard is hard to conceive at the first time. 7 if (j < p.length() && (p.charAt(j) == '?' || p.charAt(j) == s.charAt(i))) { // simple match 8 i++; 9 j++; 10 } else if (j < p.length() && p.charAt(j) == '*') { 11 asterisk = j; 12 lastMatch = i; 13 j++; 14 } else if (asterisk != -1) { // don't match or j out of bound 15 // the critical insight here is that here you need to reset i and j to the correct place 16 // by correct place I mean set j to right after '*' and i to the next char after the last char in s that matches '*' 17 j = asterisk + 1; 18 lastMatch += 1; 19 i = lastMatch; 20 } else { 21 return false; 22 } 23 } 24 while (j < p.length() && p.charAt(j) == '*') { 25 j++; 26 } 27 return j == p.length(); 28 }
这个解法第一次做稍微难以想到的是应该以怎样的出发点去进行s与p的匹配。当p用完时,可能意味着的是我们使用'*'所匹配的s中的char太少了,需要reset j到合适的位置,所以把j也放入while loop的loop guard是不合适的,因为虽然i也会在loop中大量reset,但是一旦i被用完,剩下需要检验的东西就相对简单的多了,而如果使用j做loop guard,那么就很难处理s用完了但是p还没用完的情况。