[LeetCode] Scramble String (非常规DP求解)

来源:互联网 发布:免费源码发布站程序 编辑:程序博客网 时间:2024/06/02 20:32

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") → falseisMatch("aa","aa") → trueisMatch("aaa","aa") → falseisMatch("aa", "a*") → trueisMatch("aa", ".*") → trueisMatch("ab", ".*") → trueisMatch("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和递归结合:自顶向下去递归求解,顺便弄个备忘录记录下遇到的子问题的结果。

这题从运行时间上看,发现递归+剪枝的效果最好,主要还是这题的递归程度太深了。。。

原创粉丝点击