String Algorithms
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) precisely for a pattern of length . (2)
(b) Compute the failure function array for the pattern: Show the array indexed . (4)
(c) Write from memory the pseudocode that builds in time. (4)
(d) Explain out loud (in words) the amortised argument for why the build loop is despite the inner while. (2)
Question 2 — Z-Algorithm (10 marks)
(a) Define the Z-array for a string . (2)
(b) Compute the full Z-array for: (indices ; conventionally undefined/set to or ). (4)
(c) Explain how the "Z-box" is used to avoid redundant comparisons, and give the two cases handled when . (4)
Question 3 — Rabin-Karp Rolling Hash (12 marks)
(a) Given base and modulus , write the rolling-hash update formula that computes the hash of window from the hash of window . Define every symbol. (4)
(b) Using base and modulus , treat characters as digits. For text and pattern length , compute the hash of the first window and then roll to obtain the hash of . Verify the roll gives the same value as a direct computation. (5)
(c) Explain why Rabin-Karp is expected but 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 . (3)
(c) In one or two sentences, describe the good-suffix heuristic and why using 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 , state the length of the longest palindromic substring and identify it. (3)
Question 6 — Suffix Array & LCP (8 marks)
(a) List all suffixes of 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) = length of the longest proper prefix of that is also a suffix of . "Proper" means it is not the whole substring itself. (2)
(b)
| 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 |
- (always). :
abno match → 0. -
aba→ prefixa=suffixa→ 1. -
abab→ab=2.ababa=3.ababab=4. -
abababc:cbreaks all borders → 0. -
abababca:amatches prefixa→ 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 ). Each while iteration strictly decreases k by at least 1. Since k never goes below 0, total decreases total increases . Hence total inner-loop work across all is . (2)
Question 2 — Z-Algorithm (10)
(a) = length of the longest substring starting at position that matches a prefix of (i.e. longest such that ). is conventionally (or ). (2)
(b)
| 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: : aabaa matches prefix aabaa (5 chars, runs to end) → 5. : aa = prefix aa → 2. : a → 1. (4) (½ each)
(c) The Z-box is the interval of the rightmost match of a prefix found so far (position with largest ). When computing with , let (mirror index). (4)
- Case 1: → the mirror value fits entirely inside the box, so (no comparisons needed).
- Case 2: → the box might extend further; set and continue explicit comparisons past , updating .
Question 3 — Rabin-Karp (12)
(a) Update formula (4):
- = hash of window starting at ,
- = base, = prime modulus,
- = precomputed high-order weight (remove leaving char),
- multiply by shifts left, add incoming char . Take mod ; add if negative.
(b) . (5)
First window 314: value . : , remainder . So .
Roll to 141: remove leading 3. , (since ).
. Then ; .
Direct: 141 : , remainder . ✓ Match. .
(3 for correct roll arithmetic, 2 for direct check matching.)
(c) Expected : hashes match rarely, so full character verification (needed to confirm/reject collisions) happens expected times with a good random prime. Worst case when hashes collide constantly (adversarial input or unlucky ), 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 (text char ), the bad-character rule aligns the mismatched text char with its last occurrence in the pattern to the left of : shift . If does not occur (or last occurrence is at/after ), the shift can be ; this is unsafe, so BM takes (or combines with good-suffix) to guarantee progress. (4)
(b) , last-occurrence table (3):
| char | n | e | d | l |
|---|---|---|---|---|
| last index | 0 | 5 | 3 | 4 |
(All other characters → , meaning shift past.) e last at index 5, d at 3, l at 4, n at 0.
(c) Good-suffix: after matching a suffix of the pattern before a mismatch, shift so the next occurrence of (or the longest prefix of that is a suffix of ) aligns. Taking 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 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) 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):
avsana→ 1anavsanana→ 3ananavsbanana→ 0bananavsna→ 0navsnana→ 2
LCP = [0, 1, 3, 0, 0, 2] (leading 0 convention).
(c) Prefix-doubling (sort by rank pairs over rounds, each sort or radix) gives (or naïve). LCP array uses: number of distinct substrings, longest repeated substring, or 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"}
]