动态规划法解通配符匹配算法题
Posted 算讯息
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划法解通配符匹配算法题相关的知识,希望对你有一定的参考价值。
动态规划在维基上的解释是:动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
在编程中,动态规划通常用来优化递归问题。举例说明(例题来自力扣第44题):
通配符匹配:
给定一个输入字符串(s)和一个模式(p),实现通配符模式匹配并支持'?' 和'*'。
'?' 匹配任何单个字符。
'*'匹配任何字符序列(包括空序列)。
匹配项应覆盖整个输入字符串(而非部分)。
s可以为空,并且仅包含小写字母a-z。
p可以为空,并且仅包含小写字母a-z和?及*两种字符。
例1:
输入:
s ="aa"
p ="a"
输出:false
说明:"a"与整个字符串"aa"不匹配
例2:
输入:
s ="adceb"
p ="*a*b"
输出:true
说明:第一个"*"匹配空串,而第二个"*"匹配子串"dce"。
例3:
输入:
s ="acdcb"
p ="a*c?b"
输出:false
说明:"?"只能匹配单个字符,不能匹配"dc"或者空串。
看到这题目,首先想到的是,java.util.regex.Pattern类:
public boolean isMatch(String s, String p) {
return java.util.regex.Pattern.matches(p, s);
}
但是很遗憾,java.util.regex.Pattern的匹配里,p字符串的首字符不能是'*',也不支持连续"**"这样的模式匹配。
其次比较容易想到的是用递归的方法:
public boolean isMatch3(String s, String p) {
if("*".equals(p)) return true;
if(p.isEmpty()) return s.isEmpty();
if(p.charAt(0) == '*') {
if(s.isEmpty() || p.charAt(1) == '*') {
return isMatch3(s, p.substring(1));
} else {
return isMatch3(s, p.substring(1)) || isMatch3(s.substring(1), p);
}
} else if(s.isEmpty()) {
return false;
} else if(p.charAt(0) == '?' || p.charAt(0) == s.charAt(0)){
return isMatch3(s.substring(1), p.substring(1));
}
return false;
}
第1行,如果p只剩下全匹配符,那就是true了;
第2行,如果p剩下空串了,那么如果s也空串,就是true,否则就是false;
第10行,当p不是'*'开头的字符串,而s已经是空串,那么就是false;
第12行,如果当p为'?'开头时,或者p的首字符和s的首字符相同,那么是否匹配,就要看p和s除掉首字符后是否匹配,这就是一个递归;
当p的首字符是'*',情况就比较复杂:
第5行,如果s是空串,或者p的第二个字符也为'*'时,就看p除掉首字符后是否与s匹配;
'*'可以匹配空串,所以如果p除掉首字符'*',剩余的与s匹配,说明匹配,另一种情况就是'*'可以匹配任意字符,那么把s串的字符一个一个去除,看p是否可以与其匹配,如果这种情况出现也说明匹配;这两种情况只要其中一种能匹配,那么整个就是匹配的,所以第8行是两个递归函数取或;
如果以上情况都不符合,自然就是不匹配了。
这函数虽然在部分情况下能验证s和p的匹配情况,但是这个算法的时间复杂度太大,假设s的长度为m,p的长度为n,那复杂度最糟情况有m平方*n平方。如果s和p太长了,比如:
String s = "abbabaaabbabbaababbabbbbbabbbabbbabaaaaababababbbabababaabbababaabbbbbbaaaabababbbaabbbbaabbbbababababbaabbaababaabbbababababbbbaaabbbbbabaaaabbababbbbaababaabbababbbbbababbbabaaaaaaaabbbbbaabaaababaaaabb";
String p = "**aa*****ba*a*bb**aa*ab****a*aaaaaa***a*aaaa**bbabb*b*b**aaaaaaaaa*a********ba*bbb***a*ba*bb*bb**a*b*bb";
这种情况下,这个递归最终是无法归来。
所以,用DP来优化递归问题就很必要。为了更好的理解之后的程序,先基于字符串如果匹配从头开始与从尾部开始都应该匹配这个事实,改写一下上面的程序:
public boolean isMatch2(String s, String p) {
if("*".equals(p)) return true;
if(p.isEmpty()) return s.isEmpty();
int m = s.length(), n = p.length();
if(p.charAt(n-1) == '*') {
if(s.isEmpty() || p.charAt(n-2) == '*') {
return isMatch2(s, p.substring(0,n-1));
} else {
return isMatch2(s, p.substring(0,n-1)) || isMatch2(s.substring(0,m-1), p);
}
} else if(s.isEmpty()) {
return false;
} else if(p.charAt(n-1) == '?' || p.charAt(n-1) == s.charAt(m-1)){
return isMatch2(s.substring(0,m-1), p.substring(0,n-1));
}
return false;
}
最后的字符串是否匹配,除了最后字符的比较,也取决于最后字符之前的比较,如果最后的字符是匹配的,那么只要之前的字符串都是匹配的,整个字符串就是匹配的。
有了之前的递归思想,那么DP算法就很容易理解了。
先贴出程序,再来解释:
public boolean isMatch(String s, String p) {
boolean[][] dp = new boolean[s.length()+1][p.length()+1];
dp[0][0] = true;
for (int i = 0; i < p.length(); i++) {
if (p.charAt(i) == '*') {
dp[0][i + 1] = dp[0][i];
}
}
for(int i = 0; i < s.length(); i++) {
for(int j = 0; j < p.length(); j++) {
if(p.charAt(j) == '?' || p.charAt(j) == s.charAt(i)) {
dp[i + 1][j + 1] = dp[i][j];
}
if(p.charAt(j) == '*') {
dp[i + 1][j + 1] = dp[i + 1][j] || dp[i][j + 1];
}
}
}
return dp[s.length()][p.length()];
}
dp[i][j]标志一个状态,表示s字符串前i长度的子字符串和p字符串前j长度的子字符串的匹配情况。(JAVA的序列从0开始,i长度即为charAt(0)到charAt(i-1)的长度)
dp[0][0]标志的就是当s为空串,p也为空串时的状态,这时的状态是匹配的,所以第4行dp[0][0]赋值为true;
当p为空串,s不为空时,肯定都是不匹配的,即dp[i][0]都为false。boolean变量在java里的默认值就是false,所以就无需赋值了;
而当s为空串,p不为空时,是否匹配要看'*'字符之前是否是匹配的,所以,对dp[0][j]进行初始赋值。第4到8行,对这个初始状态进行赋值。意即当s为空串时,如果p都是由'*'组成的字符串,那么是匹配的,如果有某一个字符不是'*'时,从那个字符开始,之后的都不匹配了(当然最终结果也是不匹配)。
s字符串前i+1的长度的子串是否和p字符串前j+1长度的子串是否匹配,就取决于字串最后一个字符的比较,以及之前子串是否匹配的情况,程序第10-19行就是处理这种情况:
当p子串的最后一个字符为'?'或者与s子串的最后一个字符相同时,最终是否匹配,就取决于i长度的s子串和j长度的p子串是否匹配,即dp[i + 1][j + 1] = dp[i][j];
当p子串最后一个字符为'*'时,因为p可以匹配任意字符串或者空串,所以只要是i+1长度的s子串与j长度的p子串匹配,或者是i长度的s子串与j+1长度的p子串匹配就可以了,即dp[i + 1][j + 1] = dp[i + 1][j] || dp[i][j + 1];
如果都不是,自然就不匹配,但boolean变量默认为false,无需处理。
最终dp[][]的最后一个元素就是总体两个字符串是否匹配的最终结果了。其实这也是isMatch2这个递归方法的拆解倒推,只不过用了状态数组来标志已经对比过的子串,减少了很多重复的递归计算,所以把时间复杂度减少到了O(mn)。
点击阅读全文可以获取程序的源码。https://github.com/yangmingxuan/algorithmicexamples
这个项目目前上传了上百个算法题程序源码,每周末会更新(争取)。有兴趣的朋友可以共同探讨。
以上是关于动态规划法解通配符匹配算法题的主要内容,如果未能解决你的问题,请参考以下文章