3.8.4 · D4String Algorithms

Exercises — Z-algorithm — Z-array construction, O(n+m)

2,471 words11 min readBack to topic

Throughout, recall the vocabulary the parent built:

  • is a string of length , indexed .
  • = length of the longest run starting at that also equals the start (prefix) of .
  • The Z-box is the interval with the largest we already know equals the prefix: .
  • The mirror of inside the box is .
Figure — Z-algorithm — Z-array construction, O(n+m)

Level 1 — Recognition

L1.1 — Read one Z-value

For (indices ), compute by hand. State exactly which characters you compared and why the match stopped.

Recall Solution L1.1

Position starts the run . The prefix starts .

  • Compare vs ✓ (length so far 1)
  • Compare vs ✓ (length 2)
  • Compare vs ✓ (length 3)
  • Next would be , which doesn't exist → stop.

So . The match stopped because we ran off the end of the string, not because of a mismatch.

L1.2 — Fill the whole small array

Write the complete Z-array for (length 5). Use the convention is unused (dash).

Recall Solution L1.2

Every suffix of aaaaa is itself a run of a's equal to the prefix, but capped by how many characters remain.

i 0 1 2 3 4
s a a a a a
Z 4 3 2 1

: from index 1 there are 4 characters left, all a, all match the prefix → . Each subsequent index has one fewer character remaining. Answer: .


Level 2 — Application

L2.1 — Trace with the box

Build the full Z-array for (length 8), and for each say whether the parent's Case A () or Case B () applied.

Recall Solution L2.1

Start , .

  • : Case A. Compare vs ✗ → . No box.
  • : Case A. ababab vs prefix ababab matches all 6 remaining → . Box .
  • : Case B. Mirror , , cap , ; while: vs ✗ → .
  • : Case B. , , cap , ; while: ? we test vs — out of range → stop. . New box? , not , so no update.
  • : Case B. , , cap , ; while vs ✗ → .
  • : Case B. , , cap , ; while vs out of range → stop. .
  • : Case B. , , cap , ; while vs ✗ → .

.

L2.2 — Pattern matching via Z

Text , pattern . Form , compute where (), and report the 0-indexed positions in where occurs.

Recall Solution L2.2

, length . Indices:

i 0 1 2 3 4 5 6 7 8 9 10 11
S a a b # a a b a b a a b

We need . Checking the text region ():

  • : aab vs prefix aab ✓✓✓, then vs ✗ → . Hit.
  • : aab vs aab ✓✓✓, then off end → . Hit.
  • All other positions give .

Convert with position :

  • -position .
  • -position .

So occurs at -positions . Check: ✓, ✓.


Level 3 — Analysis

L3.1 — When is a comparison "free"?

In the trace of (L2.1), at we set . How many character comparisons did the while loop actually perform there, and why? Contrast with .

Recall Solution L3.1

At : cap , so the loop entered with . It then tested s[4] == s[8] — but is out of range, so the while condition i+Z[i] < n is false immediately. Zero character comparisons. The value 4 was copied from the mirror knowledge, verified for free.

At (Case A): we started at and had to compare from scratch: 6 successful matches then off the end → 6 character comparisons. This is the "real work" that pushed from to .

Key insight: the free copy at is exactly the reuse the box buys us — the mirror at already paid for those matches.

L3.2 — Distinguish B1 from B2

In (from the parent's full trace), classify and as sub-case B1 (mirror fits inside box, no extension) or B2 (mirror reaches the edge, must extend). Justify with the numbers.

Recall Solution L3.2

At the time we process these, the box is .

  • : mirror , . Box edge distance . Is ? ✓ → B1. The mirror's match (length 1) and its terminating mismatch both lie strictly inside the box, so is copied with no comparison.
  • : mirror , . Edge distance . Is ? Yes → this is B1 as well ( fits). We copy ; the while tests vs once and fails, adding nothing.

Neither is a true B2 here, because no mirror value reached the box boundary. A B2 example: in abababab at , edge , so it hit the cap — that's the B2 shape (we relied on the min to stop us claiming unverified territory).


Level 4 — Synthesis

L4.1 — Count distinct substrings? No — count prefix occurrences

Using a single Z-array of , determine how many times the prefix ab (length 2) occurs as a substring of . Explain the rule.

Recall Solution L4.1

The prefix of length occurs starting at position iff (a match at least long begins there). We also always count position itself.

Build for ababa:

  • : vs ✗ → .
  • : aba vs aba → 3 matches, off end → . Box .
  • : , mirror , , cap , ; vs ✗ → .
  • : , mirror , , cap , ; while vs out of range → .

. Count positions (including 0) with : position (prefix itself), (). That's occurrences of ab. Check by eye: ababaab at indices and . ✔

L4.2 — Longest border via Z

The prefix that is also a suffix of (a "border") can be read from Z. For (length 10), find the length of the longest proper border. Rule: a border of length exists iff there is an index with and .

Recall Solution L4.1 → L4.2

We need positions where the match runs exactly to the end: .

Compute the relevant Z-values by hand:

  • : run abcab vs prefix abcab → matches 5 (s[5..9]=abcab, prefix abcab), then off end → , and ✓. Border length .
  • Any longer? A border of length would need with ; but . No.

Longest proper border length (abcab). Check: prefix abcab = suffix abcab of abcababcab. ✔ This is exactly the information the KMP failure function stores; Z gives an alternative route to it.


Level 5 — Mastery

L5.1 — Prove the box never shrinks

Argue rigorously that in the parent's algorithm, the value is non-decreasing across all iterations. Then use this to bound total while-loop iterations.

Recall Solution L5.1

changes only inside the box-update line, which fires iff , and sets . By its own guard, the new is strictly greater than the old . No other line touches . Therefore is (weakly) non-decreasing — in fact strictly increasing whenever it changes at all. ∎

Bound on while work. Each successful while iteration matches a character at position that was beyond the old (positions were already known, so extension only happens past ), and then advances to cover it. Since starts at , strictly increases on each such success, and is forever, there are at most successful while iterations across the entire run. Each also has at most one failing while test (the mismatch that ends its loop), giving additional constant-cost tests. Total: . Combined with the concatenation for pattern matching → .

L5.2 — Degenerate and edge inputs

State the Z-array for each edge case and explain: (a) empty string, (b) single character x, (c) two identical characters yy, (d) alphabet of one letter repeated k times (general formula).

Recall Solution L5.2
  • (a) empty string : the array is empty, . No positions to process; the loop for i in 1..n-1 runs zero times. Valid.
  • (b) x, : (only , unused). The loop starts at , so it never runs. There is no proper suffix to match.
  • (c) yy, : , Case A, vs ✓, then off end → . So .
  • (d) a^k (letter repeated times): by the L1.2 pattern, for . Formula: . This is the worst case for the naive algorithm () yet the box-based algorithm still runs in : the box jumps to cover the whole tail at , and every later is a free B1 copy.

Recall Where to go next
  • Feeling strong? Compare Z with the KMP failure function — they compute closely related border information by different mechanics.
  • For matching under hashing instead of prefix matching, see String Hashing.
  • Palindrome-flavored cousin of this reuse trick: Manacher's Algorithm.
  • Heavier full-text index structures: Suffix Array, Suffix Automaton.
  • The linear-time proof style generalizes: Amortized Analysis.
  • Hinglish walkthrough of the core: 3.8.04 Z-algorithm — Z-array construction, O(n+m) (Hinglish).