Level 3 — ProductionString Algorithms

String Algorithms

45 minutes60 marksprintable — key stays hidden on paper

Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60

Answer all questions. Show all working. Where code is requested, write clean pseudocode or a real language; correctness and complexity matter.


Question 1 — KMP Failure Function (12 marks)

(a) Define the KMP failure function (prefix function) π[i]\pi[i] precisely for a pattern PP of length mm. (2)

(b) Compute the failure function array π\pi for the pattern: P=a b a b a b c aP = \texttt{a b a b a b c a} Show the array indexed 070 \ldots 7. (4)

(c) Write from memory the pseudocode that builds π\pi in O(m)O(m) time. (4)

(d) Explain out loud (in words) the amortised argument for why the build loop is O(m)O(m) despite the inner while. (2)


Question 2 — Z-Algorithm (10 marks)

(a) Define the Z-array Z[i]Z[i] for a string SS. (2)

(b) Compute the full Z-array for: S=a a b a a b a aS = \texttt{a a b a a b a a} (indices 070 \ldots 7; Z[0]Z[0] conventionally undefined/set to 00 or nn). (4)

(c) Explain how the [L,R][L, R] "Z-box" is used to avoid redundant comparisons, and give the two cases handled when iRi \le R. (4)


Question 3 — Rabin-Karp Rolling Hash (12 marks)

(a) Given base bb and modulus qq, write the rolling-hash update formula that computes the hash of window S[i+1i+m]S[i{+}1 \ldots i{+}m] from the hash of window S[ii+m1]S[i \ldots i{+}m{-}1]. Define every symbol. (4)

(b) Using base b=10b = 10 and modulus q=13q = 13, treat characters as digits. For text T=31415T = \texttt{31415} and pattern length m=3m = 3, compute the hash of the first window 314\texttt{314} and then roll to obtain the hash of 141\texttt{141}. Verify the roll gives the same value as a direct computation. (5)

(c) Explain why Rabin-Karp is O(n+m)O(n+m) expected but O(nm)O(nm) worst case, and name the mitigation. (3)


Question 4 — Boyer-Moore Heuristics (10 marks)

(a) Explain the bad-character heuristic: what shift does it produce on a mismatch, and why can it be negative (and how is that handled)? (4)

(b) Build the bad-character last-occurrence table for pattern P=n e e d l eP = \texttt{n e e d l e}. (3)

(c) In one or two sentences, describe the good-suffix heuristic and why using max\max of both heuristics is safe. (3)


Question 5 — Manacher's Algorithm (8 marks)

(a) State the purpose of Manacher's algorithm and its time complexity. (2)

(b) Explain the transformation of inserting separators (e.g. #) and why it unifies odd/even palindromes. (3)

(c) For S=abaabaS = \texttt{abaaba}, state the length of the longest palindromic substring and identify it. (3)


Question 6 — Suffix Array & LCP (8 marks)

(a) List all suffixes of S=bananaS = \texttt{banana} with their starting indices, then give the suffix array (sorted order of starting indices). (4)

(b) Give the LCP array (LCP between consecutive suffixes in sorted order) for the suffix array above. (2)

(c) State the standard construction complexity of a suffix array via the prefix-doubling method, and one use of the LCP array. (2)


Answer keyMark scheme & solutions

Question 1 — KMP Failure Function (12)

(a) π[i]\pi[i] = length of the longest proper prefix of P[0i]P[0\ldots i] that is also a suffix of P[0i]P[0\ldots i]. "Proper" means it is not the whole substring itself. (2)

(b) P=abababcaP = a\,b\,a\,b\,a\,b\,c\,a

i 0 1 2 3 4 5 6 7
P a b a b a b c a
π 0 0 1 2 3 4 0 1
  • π[0]=0\pi[0]=0 (always). π[1]\pi[1]: ab no match → 0.
  • π[2]\pi[2] aba → prefix a=suffix a → 1.
  • π[3]\pi[3] ababab=2. π[4]\pi[4] ababa=3. π[5]\pi[5] ababab=4.
  • π[6]\pi[6] abababc: c breaks all borders → 0.
  • π[7]\pi[7] abababca: a matches prefix a → 1.

Award 4 (½ each correct entry, rounded up; deduct 1 per error).

(c) Pseudocode (4):

build_pi(P):
    m = len(P)
    pi = array(m); pi[0] = 0
    k = 0
    for i = 1 to m-1:
        while k > 0 and P[i] != P[k]:
            k = pi[k-1]
        if P[i] == P[k]:
            k = k + 1
        pi[i] = k
    return pi

2 marks structure/loop, 1 for correct fallback k = pi[k-1], 1 for correct increment/assignment.

(d) Amortised: k increases by at most 1 per iteration of the outer for (total increase m\le m). Each while iteration strictly decreases k by at least 1. Since k never goes below 0, total decreases \le total increases m\le m. Hence total inner-loop work across all ii is O(m)O(m). (2)


Question 2 — Z-Algorithm (10)

(a) Z[i]Z[i] = length of the longest substring starting at position ii that matches a prefix of SS (i.e. longest kk such that S[0k1]=S[ii+k1]S[0\ldots k-1] = S[i\ldots i+k-1]). Z[0]Z[0] is conventionally 00 (or nn). (2)

(b) S=aabaabaaS = a\,a\,b\,a\,a\,b\,a\,a

i 0 1 2 3 4 5 6 7
S a a b a a b a a
Z 0 1 0 5 1 0 2 1

Checks: Z[3]Z[3]: aabaa matches prefix aabaa (5 chars, runs to end) → 5. Z[6]Z[6]: aa = prefix aa → 2. Z[7]Z[7]: a → 1. (4) (½ each)

(c) The Z-box [L,R][L,R] is the interval of the rightmost match of a prefix found so far (position with largest i+Z[i]1i+Z[i]-1). When computing Z[i]Z[i] with iRi \le R, let k=iLk = i - L (mirror index). (4)

  • Case 1: Z[k]<Ri+1Z[k] < R - i + 1 → the mirror value fits entirely inside the box, so Z[i]=Z[k]Z[i] = Z[k] (no comparisons needed).
  • Case 2: Z[k]Ri+1Z[k] \ge R - i + 1 → the box might extend further; set Z[i]=Ri+1Z[i] = R-i+1 and continue explicit comparisons past RR, updating [L,R][L,R].

Question 3 — Rabin-Karp (12)

(a) Update formula (4): hi+1=((hiS[i]bm1)b+S[i+m])modqh_{i+1} = \big( (h_i - S[i]\cdot b^{m-1})\cdot b + S[i+m] \big) \bmod q

  • hih_i = hash of window starting at ii,
  • bb = base, qq = prime modulus,
  • bm1b^{m-1} = precomputed high-order weight (remove leaving char),
  • multiply by bb shifts left, add incoming char S[i+m]S[i+m]. Take mod qq; add qq if negative.

(b) b=10,q=13,m=3b=10, q=13, m=3. (5)

First window 314: value =314= 314. 314mod13314 \bmod 13: 13×24=31213\times24 = 312, remainder 22. So h0=2h_0 = 2.

Roll to 141: remove leading 3. bm1=102=100b^{m-1}=10^2=100, 100mod13=9100 \bmod 13 = 9 (since 13×7=9113\times7=91). h1=((h039)10+1)mod13=((227)10+1)mod13.h_1 = \big((h_0 - 3\cdot 9)\cdot 10 + 1\big) \bmod 13 = ((2 - 27)\cdot 10 + 1)\bmod 13. 227=2525+26=1(mod13)2-27 = -25 \equiv -25 + 26 = 1 \pmod{13}. Then (110+1)=11(1\cdot 10 + 1)=11; 11mod13=1111 \bmod 13 = 11.

Direct: 141 mod13\bmod 13: 13×10=13013\times10=130, remainder 1111. ✓ Match. h1=11h_1 = 11.

(3 for correct roll arithmetic, 2 for direct check matching.)

(c) Expected O(n+m)O(n+m): hashes match rarely, so full character verification (needed to confirm/reject collisions) happens O(1)O(1) expected times with a good random prime. Worst case O(nm)O(nm) when hashes collide constantly (adversarial input or unlucky qq), forcing verification every window. Mitigation: use a large random prime modulus (and/or double hashing) to make collisions vanishingly unlikely. (3)


Question 4 — Boyer-Moore (10)

(a) On a mismatch at pattern index jj (text char cc), the bad-character rule aligns the mismatched text char cc with its last occurrence in the pattern to the left of jj: shift =jlast[c]= j - \text{last}[c]. If cc does not occur (or last occurrence is at/after jj), the shift can be 0\le 0; this is unsafe, so BM takes max(1,shift)\max(1, \text{shift}) (or combines with good-suffix) to guarantee progress. (4)

(b) P=needleP = n\,e\,e\,d\,l\,e, last-occurrence table (3):

char n e d l
last index 0 5 3 4

(All other characters → 1-1, meaning shift past.) e last at index 5, d at 3, l at 4, n at 0.

(c) Good-suffix: after matching a suffix tt of the pattern before a mismatch, shift so the next occurrence of tt (or the longest prefix of PP that is a suffix of tt) aligns. Taking max\max of both heuristics is safe because each individually gives a shift that never skips a possible match, so the larger is still valid and skips more. (3)


Question 5 — Manacher (8)

(a) Finds the longest palindromic substring (and all palindrome radii) in O(n)O(n) time. (2)

(b) Insert a separator like # between every character and at the ends: abaaba#a#b#a#a#b#a#. Every palindrome in the transformed string is centred on a single position, so even-length and odd-length palindromes in the original are both represented as odd-length palindromes here — the algorithm only needs to handle one case (centre + radius). (3)

(c) S=S=abaaba. Longest palindromic substring is the whole string abaaba (length 6) — it reads the same forwards/backwards: a-b-a | a-b-a reversed = abaaba. ✓ (3)


Question 6 — Suffix Array & LCP (8)

(a) Suffixes of banana (4):

index suffix
0 banana
1 anana
2 nana
3 ana
4 na
5 a

Sorted lexicographically:

a       (5)
ana     (3)
anana   (1)
banana  (0)
na      (4)
nana    (2)

Suffix array = [5, 3, 1, 0, 4, 2].

(b) LCP array (LCP between consecutive sorted suffixes; first entry 0) (2):

  • a vs ana → 1
  • ana vs anana → 3
  • anana vs banana → 0
  • banana vs na → 0
  • na vs nana → 2

LCP = [0, 1, 3, 0, 0, 2] (leading 0 convention).

(c) Prefix-doubling (sort by rank pairs over logn\log n rounds, each sort O(nlogn)O(n\log n) or O(n)O(n) radix) gives O(nlogn)O(n\log n) (or O(nlog2n)O(n\log^2 n) naïve). LCP array uses: number of distinct substrings, longest repeated substring, or O(1)O(1) longest-common-extension queries. (2)


[
  {"claim":"KMP pi for ababab ca is [0,0,1,2,3,4,0,1]","code":"P='abababca'\ndef pi_f(P):\n    m=len(P); pi=[0]*m; k=0\n    for i in range(1,m):\n        while k>0 and P[i]!=P[k]: k=pi[k-1]\n        if P[i]==P[k]: k+=1\n        pi[i]=k\n    return pi\nresult = pi_f(P)==[0,0,1,2,3,4,0,1]"},
  {"claim":"Rabin-Karp roll: hash(314)%13=2, hash(141)%13=11 and roll matches","code":"b,q=10,13\nh0=314%q\nhigh=(b**2)%q\nh1=(((h0-3*high)*b+1)%q)%q\ndirect=141%q\nresult = (h0==2) and (h1==direct) and (direct==11)"},
  {"claim":"Suffix array of banana is [5,3,1,0,4,2]","code":"S='banana'\nsa=sorted(range(len(S)), key=lambda i:S[i:])\nresult = sa==[5,3,1,0,4,2]"},
  {"claim":"Longest palindromic substring of abaaba has length 6","code":"S='abaaba'\nbest=0\nfor i in range(len(S)):\n    for j in range(i,len(S)):\n        sub=S[i:j+1]\n        if sub==sub[::-1]: best=max(best,len(sub))\nresult = best==6"}
]