字符串两种前缀算法(z和kmp)

KMP字符串是数据结构课本上教过的,其实就是类似某种DP算法: `dp[i] = sz` 表示 `s[i-sz..i] = s[0..sz]` 基于KMP算法可以扩展出AC自动机算法:基于BFS算法来构建这个dp, 在AC里面这个dp叫做fail指针。在KMP里面也可以把这个叫做fail指针。KMP算法的好处就是可以只针对pattern做index, 这个和后面Z函数形成对比:Z函数如果要求解前缀的话需要同时对hay和needle同时做索引。

class KMP2:
    @staticmethod
    def build_fail(t):
        n = len(t)
        fail = [-1] * n
        p = -1
        for i in range(1, n):
            while t[i] != t[p + 1]:
                if p == -1: break
                p = fail[p]
            if t[i] == t[p + 1]:
                p += 1
            fail[i] = p
        return fail

    def __init__(self, t):
        self.t = t
        self.fail = self.build_fail(t)

    def search(self, s):
        fail, t = self.fail, self.t
        j = -1
        for i in range(len(s)):
            while s[i] != t[j + 1]:
                if j == -1: break
                j = fail[j]
            if s[i] == t[j + 1]:
                j += 1
            if (j + 1) == len(t):
                return i + 1 - len(t)
        return -1

另外一个前缀算法是Z函数,其中 `z[i]=sz` 表示从 i 开始最多可以和 0 开始匹配sz个字符串。 这里和KMP不同:KMP是知道当前位置结尾最多匹配多少,而Z函数是知道当前位置起始最多可以匹配多少。两种不同的语义都可以有效地求解出来。

def compute_z(s):
    n = len(s)
    z = [0] * n
    l, r = 0, 0
    for i in range(1, n):
        if i <= r:
            # s[l..r] = s[0..r-l]
            # s[i..r] = s[i-l..r-l]
            # 所以z[i-l]是从s[i]作为起点的最大匹配距离
            # 但是这个匹配距离也收到r的限制.
            z[i] = min(z[i - l], r - i + 1)
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            l, r = i, i + z[i]
            z[i] += 1
    return z

def index(s, p):
    m = len(p)
    z = zfunction(p + '$' + s)
    for i in range(m + 1, len(s) + len(p) + 1):
        if z[i] == m:
            return i - m - 1
    return -1