引子
思路:看到两个序列去匹配的问题,最自然的想法是双层循环尝试对齐匹配,我们假设表格数字为1代表匹配成功,0代表匹配失败。
分析:分别遍历s和p两个字符串,如果p[i] == s[j],则表示匹配成功,否则直接退出循环遍历。
用代码也很好实现
bool isMatch(char * s, char * p){
int n = strlen(s);
int m = strlen(p);
int dp[m][n];
for(int i = 0; i < m; I++)
for(int j = 0; j < n; j++)
dp[i][j] = 0;
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
if (s[j] == p[I])
dp[i][j] = 1;
else
return false;
}
}
return dp[m][n];
}
但是,事实并不会这样简单,因为有特殊符号星号('*')和问号('?')的加入,让事情变得复杂。细致分析的话可能情况很多已经无从下手了。
1、s 和 p 都为空
既然无从下手,我们就用最笨的办法,从最简单的方法逐步去逼近自然的情况,自然,最简单的情况当然是 p 和 s 均为空了。
分析:两个都为空,是匹配成功的。
结论:匹配成功
2、p为空
假设 p = '', s = 'abcd',将情况1中融合进去
分析:当 p 为空时,
1、如果 s 为空的时候,匹配成功
2、如果 s 非空的时候,匹配失败
结论:匹配失败
3、s为空
如果拿上例来类推的话,会理所当然的认为:
当 s 为空时,
1、如果 p 为空的时候,匹配成功
2、如果 p 非空的时候,匹配失败
对于情况1,当然没有疑问了;对于情况2,如果没有星号('*')、问号('?')特殊符号捣乱的话,确实也会匹配失败,但如果考虑星号('*')可以匹配空的情况的时候,如下图:
3.1 p = '*'
同样将情况1融合进来
结论:匹配成功
3.2、p为'**' 时
结论:匹配成功
也就是说,当 s 为空时,
1、如果 p 为空或者全部由星号('*')组成,匹配成功;
2、p 除了星号('*')之外还有其他的字符或者问号('?')的时候,还是需要分析一下:
3.3、 p包含星号('*')外的其他字符
3.3.1、p = '**a' 时
分析:星号('*')可以匹配空字符没有错,所以dp[0]、dp[1]都可以表示匹配成功,这个在上例中已经分析过了,可是到匹配p[2]的时候,匹配失败
结论:匹配失败
我们再调整一下星号('*')与'a'出现的先后次序
3.3.2、p = '*a*' 时
分析:同样的,dp[0] = 1,可是dp[1] = a,依然无法匹配空字符,所以dp[1] = 0,这里的区别在于虽然p[2] = '*',可是dp[2]无法延续上述的情况,因为中间被p[1]隔断了,所以这里的dp[2] = 0。如果从物理意义上解释的话,p = '*a*'应该去匹配一个字符串,字符串的格式为中间含有一个字符'a',这个字符'a'的左右两边包含任意多个由空格和任意字母组成的序列,比如 s = 'cd ef a we',但是假如s 为空,那肯定是匹配不上的。
结论:匹配失败
再调换一下星号('*')与'a'出现的先后次序
3.3.3、 p = 'a**' 时
分析:同上例一样, p = 'a**' 是用来匹配以字符'a'开头的,后面跟着包含任意多个由空格和任意字母组成的序列,比如 s = 'ad fwe',而s 为空的情况,也是匹配不上的。
结论:匹配失败
3.3.4、 p 只包含星号('*')和 '?'
同样的分析可以将上述的字符'a'换成'?',结论也是一样的
分析:我们在总结星号('*')与字母出现的顺序可以发现:只有从第一位开始连续出现星号('*')的时候,才会连续的出现1,否则就意味着1的连续性被打断,后续均为0。知道这一点,对将来的一般性模式拓展及总结很有用,我们甚至可以用代码来实现在s为空p非空情况下,匹配结果究竟是0或者1的规律。很明显p有下图三种情况,而且,这个情况很简单,我们可以直接画出结果。
总结,当 s 为空时,
1、如果 p 为空或者全部由星号('*')组成,匹配成功;
2、p 除了星号('*')之外还有其他的字符或者问号('?')的时候,匹配失败,失败点在p第一次其他字母'a-z'或者问号('?'),接下来无论出现任何字符,如果p的第一个字符不是星号('*'),则从第一个位置开始均匹配失败。
如果要将3.3节的所有情况用代码来实现的话,假设 p 的长度为m时,定义一个数组 dp[m + 1],用来记录 p 与空字符串 s 匹配情况,其中的dp[0] = 1,代表 p 也为空的时候,p与s匹配成功的状态,同时也将dp[0] = 1作为我们的初始状态。
int m = strlen(p);
int dp[m + 1];
dp[0] = 1;
for (int i = 1; i <= m; I++)
dp[i] = dp[i - 1] && p[i - 1] == '*';
4、s p 均不为空
有了上面的铺垫,我们来总结最后一种情况。其实,对于s、p均不为空的情况(假设s长度为n,p长度为m),可以将以上所述的情况整合在一起,整合的方法就是新初始化一个数组dp[m + 1][n + 1],其中的第0行和地0列的值其实就是上述所有情况的整合,而且,第0行和第0列的所有数字,可以直接算出来。我们依然从最简单的情况开始:
4.1、s = 'a',p = 'b'
分析:因为p[0] != s[0], 所以dp[1][1] = 0
结论:匹配失败
4.2、p = 'a' ,s = 'a'
分析:因为p[0] == s[0], 所以dp[1][1] = 1
结论:匹配成功
4.3、p = '*' ,s = 'a'
分析:因为p[0] = '*' , 可以匹配任意字符,所以无论s[0]为任何字母均可以匹配, 所以dp[1][1] = 1
结论:匹配成功
4.4、p = '?' ,s = 'a'
分析:因为p[0] = '?' , 可以匹配任意非空字符,所以无论s[0]为任何字母均可以匹配, 所以dp[1][1] = 1
结论:匹配成功
以上四种情况,是最简单的情况,我们当然可以通过四个if条件判断就可以得出结果,我们接下来再升级一下难度,看看能不能总结出一个规律,可以覆盖最一般的情况。
4.5、p = 'ab' ,s = 'ab'
dp的初始化结果并结合4.2分析的dp[1][1] = 1的结论可知:
分析:本例其实就是引子的简化版,但也并没有什么不同。现在还剩下三个空格需要我们补充,针对本案例,我们只希望知道dp[2][2]的值,因为p[1] == s[1] == 'b' , 所以dp[2][2] = 1。
结论:匹配成功
现在问题来了,如果我们每次遇到p[i] == s[j], 就设定dp[i][j] = 1,这个是不是我们要找的规律呢?
答案当然是不能,请看下例
4.6、p = 'cb' ,s = 'ab'
dp的初始化结果并结合4.1分析的dp[1][1] = 0的结论可知:
分析:我们直接来看dp[2][2],我们知道,我们定义的dp数组,就是来记录s和p对应位置字符的匹配情况的,针对本例,很明确,是不匹配的,虽然p[1] == s[1],但是p[0] != s[0],所以dp[2][2] 和 dp[1][1] 均等于0。
结论:匹配失败
规律一:
当p[i] == s[j] 时判定dp[i + 1][j + 1]的值时
1、如果dp[i][j] == 0, 则dp[i + 1][j + 1] = 0,表达的意思是如果s和p前一个对应位置匹配失败,当前位置的字母也跟着匹配失败
2、如果dp[i][j] == 1, 则dp[i + 1][j + 1] = 1,表达的意思是如果s和p前一个对应位置匹配成功,当前位置的字母也跟着匹配成功
这里需要解释一下的是,因为我们分别在s和p之前多加了一行和一列的用于概括s或者p为空的情况,所以当记录p[i] 和 s[j]匹配的时候,dp相应p[i] 和 s[j]的位置不是dp[i][j] 而是dp[i + 1][j + 1],dp[i][j]对应的记录的是p[i - 1] 和 s[j - 1]
上面的规律也很容易用代码实现
if(p[i] == s[j]){
if(dp[i][j] == 1)
dp[i + 1][j + 1] = 1;
else
dp[i + 1][j + 1] = 0;
}
简化一下也就是这样
if(p[i] == s[j])
dp[i + 1][j + 1] = dp[i][j];
4.7、 p = 'a?c' ,s = 'abc'
dp的初始化结果并结合4.2分析的dp[1][1] = 0的结论可知:
分析:因为问号('?')可以匹配任何非空的字母, 不难得出,当p[i] == '?'时,不管s[j]为任何非空的字母,其实都可以直接认为p[i] == s[j] ,对dp的处理都可以沿用规律一的总结,我们就暂且称这样的发现为规律二吧。
结论:匹配成功
规律二:
当p[i] == '?' 时判定dp[i + 1][j + 1]的值时
1、如果dp[i][j] == 0, 则dp[i + 1][j + 1] = 0,表达的意思是如果s和p前一个对应位置匹配失败,当前位置的字母也跟着匹配失败
2、如果dp[i][j] == 1, 则dp[i + 1][j + 1] = 1,表达的意思是如果s和p前一个对应位置匹配成功,当前位置的字母也跟着匹配成功
规律二也很容易用代码表示出来
if(p[i] == '?')
dp[i + 1][j + 1] = dp[i][j];
将规律一和规律二合并之后就变成
if(p[i] == s[j] || p[i] == '?')
dp[i + 1][j + 1] = dp[i][j];
4.8、s = 'abc', p = 'a*c'
dp的初始化结果并结合4.2分析的dp[1][1] = 1的结论可知:
分析:dp[1][1] = 1的结论我们已经反复看了几遍了,下面我们来确定dp[2][2]的值,如果参考规律一和规律二的结论,星号('*')可以匹配任何的字母,针对本例中dp[1][1] = 1的前提,dp[2][2]也应该等于1,同理dp[3][3] = 1,完美匹配。
结论:我们人眼肯定是能看出是匹配的
上面的分析,可能会让人误认为星号('*')没有特殊的地方?错!
我们继续往下看。
4.9、s = 'dec', p = 'a*c'
dp的初始化结果并结合4.2分析的dp[1][1] = 0的结论可知:
分析:如果参考规律一的结论,dp[1][1] = 0,那么dp[2][2] 也等于0吗?是的,dp[2][2] = 0,那么dp[3][3] 呢?看到p[2] == s[2] == 'c',同样根据规律一递推dp[3][3] = 0,事实上dp[3][3] 也确实应该等于 0。别着急去翻4.8,再耐心往下看两步。
结论:人眼知道是匹配失败的,但是如何让机器代码来计算还需要进一步的分析
4.10、s = 'ac', p = 'a*c'
dp的初始化结果并结合4.8分析的结论可知:
分析:我们先说结论吧,s = 'ac', p = 'a*c' 是可以匹配成功的,p[0] 匹配 s[0], p[1] 匹配空,p[2] 匹配 s[2]。可是当我们用代码来推导dp[3][2]的时候,是应该依据dp[2][2]的值吗?
结论:还是往下看吧
现在,是时候来总结一下星号('*')的真正规律了,在之前的所有讨论中,我们只关心dp数组对角的元素,原因也是因为,我们大脑更愿意先看到最理想或者最容易思考的情况,也就是s 和 p长度相等的情况,其实我们也应该关注对角线外的元素,因为题目的设计星号('*')的可以匹配任意字母甚至包括非空字符,所以这就会导致即使p和s不等长,依然可能匹配成功。
敲黑板!星号规律('*')总结开始了
星号('*')的出现把问题的复杂度升级了许多,开始短路了。
我们再来重申一下星号('*')的定义:可以匹配任意字符串(包括空字符串)。
虽然星号('*')看似很强大,但是也要考虑束缚条件,比如在例4.9中,s = 'dec', p = 'a*c' ,p[0] != s[0],当要用p[1]也就是星号('*')来做匹配的时候,自然是要考虑dp[1][1]的值。再看一个星号('*')匹配空字符的情况,巩固一下p和s不等长,依然可能匹配成功的例子。
4.11、s = 'abdc', p = 'a*c'
dp的初始化结果并结合4.8分析的结论可知:
分析:依然先说结论,本例中的 s 和 p 确实是匹配成功的,p[0] 匹配 s[0], p[1] 同时匹配 s[1] 和 s[2],p[2] 匹配 s[3]。但是我们dp[3][3]的时候,由于p[2] = 'c', s[2] = 'd',那么dp[3][3] = 0,本例特殊的点在于 m != n,我们最终是要求d[m][n],对于本例则是要求出d[3][4],可是d[3][4]怎么求呢,比照规律一,我们要知道他的上一个的匹配结果,也就是需要知道d[2][3],物理上的解释就是首先要判断它的前一个字符对是否能匹配成功。看来我们真的需要关注dp数组的所有元素的值了。
对于第一行的值,也就是用p[0]来匹配 s 各个位置的值,p[0] = a,很自然,当遍历s的时候,遇到s[i] == 'a',则dp[1][i + 1] = 1,否则dp[1][i + 1] = 0。补全之后如下图所示:
但是对于第二行的值就难办了,因为p[1] = '*',所以当p[1] 与 s[0] = 'a'匹配的时候应该等于1呢还是应该等于0呢?也许有人要说了当然等于1了,因为星号('*')的定义:可以匹配任意字符串(包括空字符串)。如果真的是这样的话,那么是不是说只要星号('*')所在的行,都应该为1?
大写的STOP
我们先跳回例4.9,按照上述方法,我们也很容易补全第一行和“第二行”如下图所示:
细心的同学发现了,我们补全第一行是没有问题的,第一行的数据值全部为0,但是我们把第二行加了双引号,权当假设之前的结论是准确的,那我们是不是可以继续把第三行补全呢?
震惊!dp[3][4] = 0
可是明明dp[3][3] = 1!
这下,我们是不是已经猜到了,虽然星号('*')可以匹配任意字符,但是武断的把星号('*')根据规律一来判断,应该是不对的。
我们把4.9 和4.11的图合并在一起看吧
两个图合并在一起之后,我们同步来看p[1] = '*',而且我们专注于求例dp[2][1],对于左右两个图,他们唯一不同的点在于dp[1][1]的值,左图的dp[1][1] = 0,而右侧的dp[1][1] = 1,原因也是因为左图的p[0] 和 s[0]确实没有匹配成功,而右图的p[0] 和 s[0]匹配成功了。
从物理意义上理解的话,当遇到星号('*')的时候:
1、如果星号('*')前面的元素(针对本例星号('*')前面的元素是'a')匹配成功(比如右图的情况),那么星号('*')所在的对应位置(本例中的dp[2][1])也可以假设是匹配成功,即右图dp[2][1] = 1;
2、如果星号('*')前面的元素(针对本例星号('*')前面的元素是'a')(比如左图的情况)那么星号('*')所在的对应位置(本例中的dp[2][1])也可以假设是匹配失败,即左图dp[2][1] = 0。
再把图更新一下啊
这样是不是说:
如果p[i] == '*',当计算dp[i + 1][j + 1]的值时,只需要去查看dp[i][j + 1]。用代码表示就是
if (p[i] == '*')
dp[i + 1][j + 1] = dp[i][j + 1];
先把假设再完善一下
应该已经发现不对了,对于左图,在星号('*')所在的行全部置0其实没问题,因为左图本身确实匹配失败了,可是右图呢,我们本来就知道右图是匹配成功了呀,我们把回去看 图4.9和4.11合并图_b ,我们现在重点关注一下左右两图dp[2][2]的位置
先说相同点:两个图中dp[2][2]正上方也就是dp[1][2]的位置均为0
再说不同点:
左图中,dp[2][2]的左方dp[2][1]和左上方dp[1][1]均为0;
右图中,dp[2][2]的左方dp[12][1]和左上方dp[1][1]均为1。
会不会有疑问,星号('*')左方和左上方的位置一定相等呢?或者说当遇到星号('*')的时候,是不是还要考虑星号('*')左方和左上方的位置的值呢?因为这两个位置确实会现实星号('*')之前的匹配情况啊。针对左右两图中的dp[2][2],其实他们左方dp[2][1]本身就是由左上方dp[1][1]值决定的,所以左方dp[2][1]已经反应了左上方dp[1][1]的匹配情况了。
我们先把dp[2][2]的值究竟是1 还是0 先搁置下来,我们来看看dp[2][2]右侧的dp[2][3]的值
先说相同点:两个图中dp[2][3]正上方也就是dp[1][3]的位置均为0,左上方也就是位置dp[1][2]均为0,dp[2][3]的左方dp[2][2]的值还不好确定
再说不同点:
完了,没有不同点,唯一的不同点就是左右两图后面的列数不一致。
可是实际上呢,我们应该隐隐约约可以看到,这两个位置的值应该也是不同的。可是这个不同怎么递推出来呢?依然是隐隐约约可以察觉,对于位置dp[2][3],其实左侧dp[2][2]的值可以反应出其之前的匹配情况的,同样的,dp[2][2]也是可以通过dp[2][1]来反应区别不同。
现在我们先把dp[2][2]的值分别填上
也就是说,
如果p[i] == '*',当计算dp[i + 1][j + 1]的值时,不仅需要去查看dp[i][j + 1],也需要看dp[i + 1][j]。经过上述一系列的分析,只需要
dp[i][j + 1] 或者dp[i + 1][j]其中一个为1,那么dp[i + 1][j + 1] 就等于 1。用代码表示如下:
if (p[i] == '*')
dp[i + 1][j + 1] = dp[i][j + 1] || dp[i + 1][j];
可是上面只是我们的推测,事实真的是这样吗?我们再把最常规的情况摆出来
4.12、当 p = '*a*b',s = 'adceb' 时的初始化结果:
综合以上的分析,全部填满之后
4.13、p = 'ab',s = 'adceb'
dp的初始化结果并结合4.8分析的结论可知:
至此,所有的分析都结束了,我们开始正式设计代码。
1、首先要获取p 和 s 的长度,假设分别为m、n;
2、声明一个数组dp[m + 1][n + 1],并对其第零行和第零列进行初始化;
3、分别对p 和 s进行遍历,将dp数组的每个位置进行赋值
有三种情况:
3.1、 如果s[i]==p[j] or p[j]=='?' 则 dp[i+1][j+1] = dp[i][j]
3.2、 如果p[j]=='*' 则dp[i+1][j+1] = dp[i][j+1] || dp[i+1][j]
3.3、 s[i] != p[j] 则 dp[i+1][j+1]=0;
4、返回结果即dp[m][n]即可
其实,这个图就是将图3.6和图2整合了一下而已。
好了,到了这一步,我们就可以来分析计算剩下来的所有空格应该对应的值了,也就是从这一步开始,我们要分析动态规划里最难找的最优子状态了。
bool isMatch(char * s, char * p){
int n = strlen(s);
int m = strlen(p);
bool dp[m + 1][n + 1];
for (int i = 0; i < m + 1; i++)
for(int j = 0; j < n + 1; j++)
dp[i][j] = 0;
dp[0][0] = true;
for (int i = 1; i <= m; i++)
dp[i][0] = dp[i - 1][0] && p[i - 1] == '*';
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
if (s[j] == p[i] || p[i] == '?'){
dp[i + 1][j + 1] = dp[i][j];
}
else if (p[i] == '*'){
dp[i + 1][j + 1] = dp[i][j + 1] || dp[i + 1][j];
}
else{
dp[i + 1][j + 1] = 0;
}
}
}
return dp[m][n];
}