Z-Box 简介

你很可能因为以下两个原因之一找到了这篇文章:

无论哪种方式,我们将研究 Z-Box - 不是使用一堆公式而是使用示例和 Python 代码。

什么是 Z-Box?

如果你在讲座中听说过 Z-Box 或在书中读到过它们,你可能遇到过

$$\begin{array}{ll}(1)&\;\text{Let }s:=s_0\cdots s_{n-1}\in\Sigma^n\\(2)&\;\forall i\in\{1\cdots n-1\}:\;Z_i:=max\{j\in[0\cdots n]: s_i\cdots s_{i+j-1}=s_0\cdots s_{j-1}\}\\(3)&\;\text{Z-Box starting at position i}:=s_i\cdots s_{i+Z_i-1}\,\text{if }Z_i\neq0\end{array}$$

但说实话。这些定义主要针对已经理解其含义的人。对大多数学生来说,这对理解算法没有太大帮助。

不管怎样,让我们分解它,看看能挽救什么。

$(1)$ 这只是说 $s$ 是长度为 $n$ 的字符串(使用此定义)。技术上 $\Sigma$ 是字符串的字母表 - 但对于所有实际意图和目的,这由编程上下文定义,所以我们忽略 $\Sigma$。

所以假设 $n=6$。那么以下每个都符合定义:

is_valid_zbox.py
s = "abc123"
s = "fo bar"
s = "      "
s = "abcabc"

在几乎所有处理字符串的算法定义中,你会发现类似的定义。

$(2)&(3)$ 稍微复杂一些。我将先不讨论定义来解释它。

这是简单的部分:从位置 i 开始的 Z-Box 只是 s 的子字符串,从位置 $i$ 开始。$Z_i$ 是其长度。

不幸的是,困难的部分是它不只是从位置 $i$ 开始的任何子字符串。但我们如何检查 Z-Box 是否有效?

让我们看看一段 Python 代码片段:

zbox_algorithm.py
def is_valid_zbox(s, i, zi):
    # zi 是 zbox 的长度,计算 zbox
    zbox = s[i:i + zi]
    # 计算相同长度的前缀
    prefix = s[0:zi]
    return zbox == prefix

is_valid_zbox("abcabc", 2, 2) # "ca" != "ab" => False
is_valid_zbox("abcabc", 2, 3) # "cab" != "abc" => False
is_valid_zbox("abcabc", 3, 2) # "ab" == "ab" => True
is_valid_zbox("abcabc", 3, 3) # "abc" == "abc" => True

所以如果两个字符串相等,Z-Box 就是有效的:Z-Box 本身和 s 的前缀

正如我们在示例中看到的,在任何位置 $i$ 可以有多个有效的 Z-box。我们使用哪一个? 由于 $(2)$ 中的 max,我们总是使用最长的一个。

简单的 Z-Box 算法

以下是如何使用我们目前所知计算 Z-box

zbox_examples.py
def is_valid_zbox(s, i, zi):
    # zi 是 zbox 的长度,计算 zbox
    zbox = s[i:i + zi]
    # 计算相同长度的前缀
    prefix = s[0:zi]
    return zbox == prefix

def zbox(s, i):
    # 从位置 i 开始的子字符串的最大长度
    # (s 只有这么多字符)
    maxlen = len(s) - i
    # 尝试每个 zbox,从最长的开始
    # 即 maxlen, maxlen-1, ..., 1
    for zi in range(maxlen, 0, -1):
        if is_valid_zbox(s, i, zi):
            # 返回第一个有效的 zbox
            # 由于我们从最长的开始,
            #  这总是最长的 zbox
            zbox = s[i:i + zi]
            return zbox

# 计算 zbox
s = "abcabc"
[zbox(s, i) for i in range(1, len(s))]
# [None, None, 'abc', None, None]

为什么结果中有这么多 None 值?因为正如你在字符串中看到的,a,字符串中的第一个字符,只在位置 3 再次出现。

为什么我们只计算从 $[1\cdots n-1]$ 开始的 Z-box?我们也可以为 $[0\cdots n-1]$ 计算!?!? 但是如果你取 $s$ 的任何长度为 $j$ 从 0 开始的子字符串,并将其与 $s$ 的长度为 $j$ 的前缀比较 - 它们是相同的,无论多长!这是因为 $s$ 的长度为 $j$ 的前缀就是从 0 开始的长度为 $j$ 的 $s$(pre 意为之前,因此我们从 0 开始!)

让我们看看另一个示例:

zbox_ananas_example.py
# 计算 zbox
s = "ananas"
[zbox(s, i) for i in range(1, len(s))]
# [None, 'ana', None, 'a', None]

还有一些示例我们找不到任何 Z-box!

zbox_foobar_example.py
# 计算 zbox
s = "foobar"
[zbox(s, i) for i in range(1, len(s))]
# [None, None, None, None, None]

但对于某些我们可以找到很多!

zbox_many_example.py
# 计算 zbox
s = "abaabaabab"
[zbox(s, i) for i in range(1, len(s))]
# [None, 'a', 'abaaba', None, 'a', 'aba', None, 'ab', None]

额外阅读:

另一篇关于 Z-box 的易于理解的博客文章,包含更高效的算法

讲座类型材料:

https://www.bio.ifi.lmu.de/mitarbeiter/volker-heun/notes/ab6.pdf section 2.5.1 https://www.informatik.hu-berlin.de/de/forschung/gebiete/wbi/teaching/archive/ws0506/bioinformatik/03_zbox.pdf


Check out similar posts by category: Algorithms