定义如下通配符:
? : 代表任意出现了一次的字符;
* : 代表出现任意长度的字符串,长度可以为0;
现在,研究一下这两个符号的代数关系,定义如下代数结构:
+_0 = * : 代表出现任意长度至少为0的字符串;
+_1 = ?* = *? = ?+_0 = +_0? : 代表出现任意长度至少为1的字符串;
+_k = ?+_(k-1) = +_(k-1)? : 代表出现任意长度至少为k的字符串;
并且存在以下代数关系:
+_n +_m = +_{n+m}
这样,根据以上代数结构,就可以把输入模式串化简为只含+_n, ?和已知字符的组合。接下来,根据通常做法,我们开始试图构建用于字符串匹配的自动机,很明显,对于已知字符和?,状态转移是明确的,但是对于+_n这样的不确定通配符,状态转移明显没有统一的标准,考虑以下例子:
输入串: {长度为n的不含x的已知字符串}xb{长度为m-2的不含x的已知字符串}xc.
模式串A:+_{m+n}xc
模式串B: +_nxb+_mxc
很明显,两个模式都应该得到匹配,但是我们却无法确立一个统一的标准。比如说,当你的状态机当前模式是+_k时,你唯一知道的就是当遇到某个x时,匹配应当终止,但是却无法确定究竟选择哪一个x:是选择最远出现的那个x还是最近出现的?在以上的例子中,任意一种选择都会在满足一个模式串的时候违背另一个。
出现这个困境的原因在于,当我们仅仅依靠x的位置信息时,是无法确定匹配是否应当终止的,我们还必须要“往后看”。也就是说,仅仅检查x是否匹配是不够的,还应当进一步检查x以后的串是否匹配了——事实上,我们在检查两个+_之间的串是否存在匹配:具体的说,如果我们有串+_m{s}+_n,那么实际上,我们就是去输入串中找{s},但是必须保证找到的{s}前面至少有m个字符,后面至少有n个字符,但是通常来说,情况要更复杂,模式串一般具有如下标准形式:
+_{m1}{s1}+_{m2}{s2}...+_{mk}{sk}...+_{mn}{sn},
所以,所谓通配符匹配,实际上就是找到所有可能的{si}的位置,并且这些位置的安排不和+_{mi},+_{mi+1}所造成的约束产生冲突. 目前来看,能解决这个问题的只有搜索算法。因为这相当于求解si.pos的合理取值(所有可能的取值来源于对si的模式匹配),以满足以下不等式组:
si.pos>=mi+1
si.pos<=L-mi
s(i+1).pos>=si.pos+si.length+m(i+1) (*)
注意到,搜索的主要时间将花费在求解(*)式上,这是一个差分约束系统。如何求解这个约束系统?如果我们直接通过类似KMP那样的算法找出所有可能的si.pos取值,则必定会浪费大量的计算时间,因为有些取值本身就导致si之间互相重合。
字符黑洞:
这是一种更有效的方法,直接利用(*)式的单调性——如果我们已知了s(i-1).pos,那么确定si.pos的过程无非就是确定+_(mi)终止时,对应的字符匹配是哪一位,换句话说,如果遇到的字符是不匹配的,那么我们完全可以将它纳入+_所涵盖的字符串中,然后在从头开始试图匹配。这个过程就像一个字符黑洞在不断的吞噬无法匹配的字符,直到最后完全匹配。那么,这种贪心策略是正确的吗?事实上,我们可以用一个自动机来描述这个黑洞的行为:
黑洞起始状态为+_i后第一个已知字符;
随后的状态转移,当读入字符和模式{si}完全比配时才进入下一状态,否则统统退回起始状态(这时已读入的字符被认为吸入黑洞中);
可见模式si要么被完全匹配,所以si.pos也可以确定了,可以进入下一阶段(黑洞+_(i+1));要么匹配失败,被吸入黑洞吞噬的字符使得剩下的字符长度已不足以构建完整的si串以及其后面的+_m(i+1)。