判断字符串str和pattern是否匹配?
提示:这道题非常困难,但是也要分清楚细节,搞清楚暴力递归的定义,摸清如何修改动态规划代码
题目
字符串str只包含a–z的小写字母,且0<=str.len<=100,pattern除了含有小写字母之外,还有.或x*,【x是任意a–z】。
.可以变为单个字符(a–z任意变);
x*可以变为0–N个字符(无限制长度的);
请问字符串str能否与pattern匹配?
一、审题
换句话说:
pattern,能否通过变换.和x*,变为str?是true,否则false
首先我们来看
.必须变一个单字符,a–z,不能变没了;所以有.的单独出现,就必须有一个字符变出来;
比如:
str=abcde,pattern=abc.e=abcde
x星 遇到了,则显然不能是**,这是不合法的串
x星 遇到了,可以是 . 星 ,比如str=abcde,pattern= . 星 = abcde
x星 可以变没了,也可以变很多不同的字符出来。
比如str=abcde,pattern= ab . 星 cde = abcde
str=abcde,pattern= abcde = abcde,只变出1个a来
str=aaabcde,pattern= aabcde = aaabcde,变出2个a来
等等
二、解题
遇到这个题呢,需要熟悉它,然后写出暴力递归的代码,再转化为DP填表的动态规划代码
首先,咱们得检查str和pattern的合法性,因为:
str中不许有.和*
其次,pattern中,不许*做开头,因为必须出现x星配合使用;
另外,**不能同时出现,否则违规,必须是x星;
//检查字符串的合法性,str必须没有*与.,p才能有,但是p不能出现**
public static boolean isValid(char[] s, char[] p){
for (int i = 0; i < s.length; i++) {
if (s[i] == '.' || s[i] == '*') return false;
}
for (int i = 0; i < p.length; i++) {
if (p[i] == '*' && (i == 0 || p[i - 1] == '*')) return false;//*在开头或者**出现都不行
//一旦出现在开头,直接返回false
//不是开头才i-1查一下是否为*
}
//都查了合法
return true;
}
下面开始尝试暴力递归,既然是2个字符串,往往是一个样本做行,一个样本做列的样本位置对应模型;讨论某个位置i做开头,或者i做结尾时的情况;
以往可能经常会尝试pattern的0–j上可以变为str的0–i吗?
但是仔细一想,咱们经常会遇到i位置是x*
也就是说可能pattern的j位置是x,我们需要看j+1位置是不是*,所以讨论0–j是不合理的,我们需要讨论j–N-1位置!!!
因此定义这么一个暴力递归的函数:f(str,i,pattern,j)
str长为N,pattern长为M
f定义为pattern的j–M-1范围内的字符串,是否可以变为str的i–N-1范围上的字符串呢?
有了这样的f=process定义,主函数这么调用:
//主函数
public static boolean isMatch(String s, String p){
if (s == null || p == null) return false;
char[] str = s.toCharArray();
char[] ptr = p.toCharArray();
return isValid(str, ptr) && process(str, ptr, 0, 0);//从0位置开始匹配能匹配上吗,合法的情况下
}
process函数如何写:
自然要分各种情况讨论:
(1)当j=M时,意味着pattern的M–M是一个空串""
此时,要行让"“空串变为str的i–N-1的话,必然也只能变为”“空串了,也就是说str的i–N-1=”",即i==N呗。
(2)来到j位置,如果j+1=M-1,pattern[j+1] 不是星,看j+1处pattern的字符啥状况:
故j+1处,可能是普通的字符,也可以是.
既然pattern[j+1]它不是星,而且是最后一个字符,那么str[i]位置必然不能是N位置(即i != N),至少也是N-1位置,即保证str[i]有一个字符,
且:想让pattern变成str,那得保证(str[i]=pattern[j] 或者 pattern[j]=.)
且保证f(i+1,j+1)是true;
(3)来到j位置,pattern[j+1 ]是星呢?
这里已经潜在有一个条件,如果j+1是星,那么j处一定不是星,之前已经约定好了
——如果str[i]=pattern[j],或者pattern[j]=.
那类似于x星或者.星,这俩可以变为1–无穷个str[i–N-1]
比如:.星能变出0个a,1个a,也可以变出2个a,甚至更多的a来,这都是合理的
那就只需要再看看f(i,j+2),f(i+1,j+2),f(i+2,j+2),……f(i+无穷,j+2)
这啥意思?就是让pattern[j+2–M-1]那些字符,去匹配str[i–N-1],str[i+1–N-1],str[i+2–N-1],……str[N-1–N-1]的字符串
意味着,我们用.星,或者x星,变成了012–无穷个str中间那些字符【这不就是x星的规则吗,很容易理解】
——注意,万一pattern[j]!=str[i]呢,那你x星就只能变出0个a来,直接看f(i,j+2)就行,啥意思?就是让pattern[j+2–M-1]那些字符,去匹配str[i–N-1]字符,让pattern中j,j+1对应的x星直接变没了。否则对不上str的。
这个代码,要仔细领略:
//暴力递归
//str从i位置到结尾,ptr从j位置到结尾,能匹配上吗,也就是p能变成s吗
public static boolean process(char[] str, char[] ptr, int i, int j){
//当两者同时到结尾,可以认为没问题
if (j == ptr.length) return i == str.length;
//然后分两种情况,一个是j+1位置非*
//既然j+1就不是*,那j位置必须要和i匹配,要么是相等,要么是.,否则配不上,当然保证i还有东西才行,且,后续位置也能配上才可以哦
if (j + 1 == ptr.length || ptr[j + 1] != '*') return i != str.length &&//s不能提前耗尽了
(ptr[j] == '.' || ptr[j] == str[i]) &&//相等
process(str, ptr, i + 1, j + 1);//后续
//然后是第二种情况,一个是j+1位置恰好就是*
//1)加入ptrj!=stri,这个时候,j*必须变为0个i字符才行
//2)当ptrj==stri的话,有很多种无限情况,j*可能变为0个字符,1个,2个,……无穷个,直到你前缀不再是ptrj为止
while (i != str.length && (ptr[j] == str[i] || ptr[j] == '.')){
//如果等,或者是.,都看看变0,1,2,3,4个能成吗,通过先调用process,再加i的办法来实现
if (process(str, ptr, i, j + 2)) return true;//如果j*变0个,就可以,直接就可以了
//否则得递增
i++;//让i+1,2,3等等等,含义就是匹配str[i + k] ptr[j + 2],能行吗,作用是j*变k个j出来能匹配str吗
}
return process(str, ptr, i, j + 2);//j*必须变为0个i字符
}
这个题理解暴力递归就够难的了,转DP今后复习再来转,先把暴力递归捋清楚,f的定义很重要。
后面我再来重新写下面dp代码怎么来的:
可以跳过下面的代码:【不过不管怎样,我还是硬着头皮讲一下这个代码怎么来的?】
在动态规划dp填表过程中,暴力递归怎么写?表就怎么填
我们看process函数:
2个样本,ij,里面j取到了M,i取到了N,主函数调f(0,0),故我们需要填表dp的为N+1 * M+1的表格
让str做行,i就是行下标
让pattern做列,j就是列下标
我们最后要dp[0][0] :代表str[0–N-1]能否由pattern[0–M-1]变化匹配而来???true or false
看下面的图,咱们要dp[0][0],肯定要从最后一样,最后一列开始填,然后找dp[i][j]依赖谁?
然倒回来填表。
(1)看N,M那个格子,代表pattern[M]=“”,空串,它能生成str[i–M-1]哪些字符串呢?
它一个空串,只能生成str中的一个空串“”,即i=N那个地方,代表N–N就一个空串,故dp[N][M]=true;
(2)看M列其他格子,空串不能生成有字符的字符串,所以趋势false;
(3)看N行,M-1那个格子,由于M-1只有pattern的一个字符,无法组成x星这种,所以不可能变为str的哪个字符串的,即false;
(4)看N-1行,M-1列那个格子,既然是对应位最后一个字符,如果pattern[M-1]=str[N-1]或者,pattern[M-1]=.,这样pattern[M-1]还是可以变str[N-1]的,否则就是false。
(5)看M-1列的其他格子,由于M-1列也就对应pattern一个字符,不可能出现x星,故i行M-1列都为false。一个字符不可能变出更多字符来。
(6)看N行其他的格子,str是空串“”,而pattern中,至少j<=M-1可能会有x星这种出现,因此只要是j+1位置为一个星,那无论前面j是啥,x星,或者.星,都能当做x星,让其变为空串“”,故填表时,看j+1位置的情况(pattern中的j+1位置的字符);
填表时,从右往左填表,当j处非星,且j+1处事*,则dp[N][j]=true;一旦有一个是false,前面的都不用去填了,默认就是false;
——上面这些格子,我们初始化一下:
//填表的函数可以单独列出来,简介
public static boolean[][] initDP(char[] str, char[] ptr){
int N = str.length;
int M = ptr.length;
boolean[][] dp = new boolean[N + 1][M + 1];//因为终结位置是要用的
//最后一列,只看最后一行交叉的位置
dp[N][M] = true;//没的说,basecase,双双都是结尾,耗尽,空串是匹配的
//最后一行--因为最后倒数第二列,M-1需要填写,所以我从右边开始判断,j*是否一直不断
//j*j*出现,可以让exp直接变为0个字符,这样就是str耗尽的空串状况
for (int j = M - 2; j >= 0; j -= 2) {
//每次跳2跳核查
if (ptr[j] != '*' && ptr[j + 1] == '*') dp[N][j] = true;//j*格式就没问题
else break;//一旦出现不适j*的格式,之前的格子都不行了
}
//M-1列,先看最后一个格子dp[N][M-1]因为j是结尾字符,但是此刻str耗尽了已经
//故,只会是F
dp[N][M - 1] = false;
//再看倒数第2行dp[N-1][M-1],俩都是最后一个字符,exp是.或者与stri相等就行,否则F
dp[N - 1][M - 1] = ptr[M - 1] == '.' || ptr[M - 1] == str[N - 1];
//其他的格子,代表啥呢,j是最后一个位置,但是i还有很多字符,不是结尾位置,显然不能变为这么多字符,且违反了basecase,全F
return dp;
}
(7)一圈都填好了,请问任意位置ij怎么填呢?
看process,显然dp[i][j]会依赖i+1,j+1位置,这是当j+1是非星的时候的那段代码;【上面图中ij的右下角那个格子】
dp[i][j]还会依赖i+0,1,2,3等这些行,j+2列,【上面图中j+2那一列全部都依赖】
然后看process怎么填,咱就怎么填。
//改DP,既然是两个字符串,显然就是样本位置对应模型
//str做行,exp做列,dpij表示:str从i到N能匹配ptr的j到M吗
//N=str.len,M=ptr.len
//这里要搞清楚这个表怎么填写,就要根据暴力递归的依赖,才知道ij如何填,这样才知道ij依赖哪些位置
//basecase中,当j==M时,看i==N与否,就知道dp[i][M]是T/F了
//注意递归中暴力递归调用了这俩函数:process(str, ptr, i + 1, j + 1);
//(process(str, ptr, i, j + 2))。故 ij依赖谁呢,右下角,和while中的i,j+2这一列
//你草稿纸画一个表,就知道这个表得填倒数第二列,最后一行,才行,否则咱们搞不定
//最后主函数,需要return dp[0][0],所以是从下往上,从右往左填表的
//dp改编
public static boolean isMatchDP(String s, String p){
if (s == null || p == null) return false;
char[] str = s.toCharArray();
char[] ptr = p.toCharArray();
if (!isValid(str, ptr)) return false;//检查合法性
boolean[][] dp = initDP(str, ptr);//先填别的格子
int N = str.length;
int M = ptr.length;
//然后倒回来填
for (int i = N - 1; i >= 0; i--) {
for (int j = M - 2; j >= 0; j--) {
//暴力递归怎么填,这里就怎么填,但是i!=N了,已经不要考虑上面的这个条件了
//然后分两种情况,一个是j+1位置非*
//既然j+1就不是*,那j位置必须要和i匹配,要么是相等,要么是.,否则配不上,当然保证i还有东西才行,且,后续位置也能配上才可以哦
if (ptr[j + 1] != '*') dp[i][j] = (ptr[j] == '.' || ptr[j] == str[i]) && dp[i + 1][j + 1];//后续
else {
//带j*
//然后是第二种情况,一个是j+1位置恰好就是*
//1)加入ptrj!=stri,这个时候,j*必须变为0个i字符才行
//2)当ptrj==stri的话,有很多种无限情况,j*可能变为0个字符,1个,2个,……无穷个,直到你前缀不再是ptrj为止
int si = i;
while (si != str.length && (ptr[j] == str[si] || ptr[j] == '.')){
//如果等,或者是.,都看看变0,1,2,3,4个能成吗,通过先调用process,再加i的办法来实现
if (dp[si][j + 2]) {
dp[i][j] = true;//如果j*变0个,就可以,直接就可以了
break;
}
//否则得递增
si++;//让i+1,2,3等等等,含义就是匹配str[i + k] ptr[j + 2],能行吗,作用是j*变k个j出来能匹配str吗
}
//上面都是false的话,看
if (dp[i][j] == false) dp[i][j] = dp[si][j + 2];//j*必须变为0个i字符的情况
}
}
}
return dp[0][0];
}
本表格能填出来的前提,是一定理解透了我们写的出来的暴力递归代码的含义,
尤其理解f是从pattern的j–M-1范围上去变str的i–N-1范围。
测试代码:
public static void test(){
String s = "aab";
String p = "c*k*a*b*";
System.out.println(isMatch(s, p));
System.out.println(isMatchDP(s, p));
}
public static void main(String[] args) {
test();
}
总结
提示:重要经验:
1)暴力递归写样本位置对应模型,在尝试时发现,j位置可能要考虑j+1位置的情况,那就需要定义为j–M-1上的匹配;
2)而在尝试时发现,j位置可能要考虑j-1位置的情况,那就需要定义为0–j上的匹配;
这俩过程需要磨炼,大多数情况下是情况2,但是这个题目就是说的1),要仔细理解。