Z algorithmの文章による説明

Z algorithmを図に頼らずに理解したかったので書きました.

長さ N (\geq 1)の文字列 SにZ algorithmを適用することを考えます.

以下の説明では文字列中の各文字を0-basedで指定します1 S i文字目を S[i]と表記します.また, S l文字目から r-1文字目までを取り出した部分文字列を S[l:r]と表記します. l=rのとき s[l:r]は空文字列とし,空文字列同士は一致するとします.便宜上 S[N]を任意の文字に一致しない文字と取り決めます.

 i=0,1,\cdots,N-1に対し, Z[i] S[0:j]=S[i:i+j]かつ S[j] \neq S[i+j]を満たす N-i以下の非負整数とします.つまり, Z[i] S S[i:N]の最長共通接頭辞の長さを表します. S=S[0:N]なので Z[0]=Nです.

以下, N \geq 2とし,ある i (\geq 1)について Z[0], Z[1], \cdots, Z[i]が既知であるとします.また, j=Z[i]とします. Z[i]の定義より, S[0:j]=S[i:i+j]かつ S[j] \neq S[i+j]です.

 k=1, 2, \cdots, j-1について,この順に Z[i+k]を求めることを考えます. i+k>kより Z[i+k]を求めるときには Z[k]は既知であり,この値を利用します.各 kについて, k+Z[k]の値により以下のように場合分けして考えます.

  •  k+Z[k] \lt jのとき

     S[0:j]=S[i:i+j]より, [k:k+Z[k]+1]=S[i+k:i+k+Z[k]+1]なので以下の2つの関係が成り立ちます.

    •  S[0:Z[k]]=S[k:k+Z[k]]=S[i+k:i+k+Z[k]]
    •  S[Z[k]] \neq S[k+Z[k]] = S[i+k+Z[k]]

     S k文字目以降の Z[k]+1文字と S i+k文字目以降の Z[k]+1文字とが, Sの接頭辞と Z[k]文字目で不一致となることも含めて一致しているので Z[i+k]=Z[k]であると分かります2

  •  k+Z[k] \geq jのとき

     S[0:j]=S[i:i+j]及び Z[k] \geq j-kより, S[0:j-k]=S[k:j]=S[i+k:i+j]が成り立ち, Z[i+k] \geq j-kであると分かります. Z[i+k]を確定するために S S[i+k]を比較する必要がありますが,最初の j-k文字については既に一致していることが判明しているため,比較処理を省略することができます.

Z algorithmでは k+Z[k] \geq jとなる kが見つかった時点で i i+kで置き換え, 最初の j-k文字の比較処理を省略して Z[i]を求めることで Z[0], Z[1], \cdots, Z[i]が既知である最初の状態に戻します.このようにすることで文字を比較する位置が後退せず,全体の計算量が \Theta(N)になります.

実装例 (Rust)

pub fn z_algorithm<T>(seq: &[T]) -> Vec<usize>
where
    T: Eq,
{
    if seq.is_empty() {
        return vec![];
    }

    let n = seq.len();

    let mut lengths = vec![0; n];
    lengths[0] = n;

    let mut cursor = 1;
    let mut common_len = 0;
    while cursor < n {
        while cursor + common_len < n && seq[cursor + common_len] == seq[common_len] {
            common_len += 1;
        }

        if common_len == 0 {
            cursor += 1;
            continue;
        }

        lengths[cursor] = common_len;

        let mut shift = 1;
        while shift + lengths[shift] < common_len {
            lengths[cursor + shift] = lengths[shift];
            shift += 1;
        }

        cursor += shift;
        common_len -= shift;
    }

    lengths
}


  1.  Sの先頭の文字は 0文字目,最後の文字は N-1文字目となります.
  2. 人の課題を写したら全く同じ間違え方をしていてバレたという話と少し似ています.