Implement regular expression matching with support for '.'
and '*'
.
'.' Matches any single character.
'*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
The function prototype should be:
bool isMatch(const char *s, const char *p)
Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true
这题的递归思路非常明显,大致思路:
对于s1和s2,每个都划分成两个子字符串。分别叫作s1_left, s1_right, s2_left, 和s2_right。如果s1和s2是scramble,那么只有两种情况:
1) s1_left和s2_left是scramble,并且s1_left和s2_left是scramble;
2) s1_left和s2_right是scramble,并且s1_right和s2_left是scramble.
考虑到递归这里会对子问题进行重复求解,于是只能得用DP。如果使用常规的DP方法,则需要3维的DP:dp[i][j][k]表示s1从i开始,s2从j开始,长度为k的两个子字符串是否为scramble。
不过以上解法非常的麻烦,而且其实这种3维的DP表里仍然会存储冗余的状态。自己想出了一种非常规的DP求解方法,发现可以用一个哈希表代替3维的DP表,减少对冗余状态的存储。另外这里的DP其实可以跟递归思路一样,自顶向下,而非所谓真正的自底向上的DP。
这里记录子问题求解结果的数据结构我用Map<String, Boolean>,前面其实偷了一下懒,理论上应该是类似于Pair<String, String>一样的东西,就是任意两个字符串看成一个pair,然后记录是否为scramble的结果。但是如果真的手动写个类,还有覆盖equals方法,太麻烦了,于是这里偷懒一下,把Pair<s1, s2>的形式替换成s1@s2,记成一种特殊字符串。
另外,这里提供了几个快速的递归出口,一个是如果两个字符串长度不相等,可以立刻返回false。另一个是如果两个字符串内容相等,则可以立刻返回true。
// Memo for dp.
private Map<String, Boolean> dp = new HashMap<String, Boolean>();
public boolean isScramble(String s1, String s2) {
String key = s1 + "@" + s2;
if (dp.containsKey(key)) {
return dp.get(key);
}
// Two quick recursion exits.
if (s1.length() != s2.length()) {
dp.put(key, false);
return false;
}
if (s1.equals(s2)) {
dp.put(key, true);
return true;
}
// Partition and match recursively.
for (int i = 1; i < s1.length(); ++i) {
for (int j = 1; j < s2.length(); ++j) {
// i and j are the partition indexes.
String s1_left = s1.substring(0, i);
String s1_right = s1.substring(i);
String s2_left = s2.substring(0, j);
String s2_right = s2.substring(j);
if (isScramble2(s1_left, s2_right)
&& isScramble2(s1_right, s2_left)
|| isScramble2(s1_left, s2_left)
&& isScramble2(s1_right, s2_right)) {
return true;
}
}
}
dp.put(key, false);
return false;
}
其实这题除了使用DP,还可以使用
递归+剪枝的方法。简单的递归虽然会导致TLE,但是有效剪枝有时还可以达到比DP更好的效果。
这里的剪枝条件可以
简单设为,所有字符的ASCII的值之和必须相等,这是成为scramble的一个充分条件。代码如下
public boolean isScramble2(String s1, String s2) {
// Two quick recursion exits.
if (s1.length() != s2.length())
return false;
if (s1.equals(s2))
return true;
// Prune candidates here to reduce candidate check times.
int sum = 0;
for (int i = 0; i < s1.length(); i++) {
sum += s1.charAt(i) - 'a';
sum -= s2.charAt(i) - 'a';
}
if (sum != 0)
return false;
// Partition and match recursively.
for (int i = 1; i < s1.length(); ++i) {
for (int j = 1; j < s2.length(); ++j) {
// i and j are the partition indexes.
String s1_left = s1.substring(0, i);
String s1_right = s1.substring(i);
String s2_left = s2.substring(0, j);
String s2_right = s2.substring(j);
if (isScramble2(s1_left, s2_right)
&& isScramble2(s1_right, s2_left)
|| isScramble2(s1_left, s2_left)
&& isScramble2(s1_right, s2_right)) {
return true;
}
}
}
return false;
}
总结一下此题。一般而言,求最优解的题目一般就2种思路:
1)如果足够幸运,能够发现贪心性质,则应该优先选用贪心法;(这题显然没有贪心性质,可以忽略。)
2)否则,寻找递归性质,尝试递归求解。
对于2),这里首先看子问题是否会被重复求解。如果存在重复的子问题求解,那么其实可以有3中选择:
1)DP是常规思路;
2)递归+剪枝 (当然用DP的时候也是可以剪枝的啦);
3)DP和递归结合:自顶向下去递归求解,顺便弄个备忘录记录下遇到的子问题的结果。
这题从运行时间上看,发现递归+剪枝的效果最好,主要还是这题的递归程度太深了。。。