String Algorithms
Level: 4 (Application — novel problems, no hints) Time limit: 60 minutes Total marks: 60
Answer all questions. Show all working. Where an algorithm is requested, state the invariant or recurrence you rely on.
Question 1 — KMP failure function & borders (12 marks)
Consider the pattern P = "abacabab".
(a) Construct the KMP failure (prefix) function array for P, showing each entry. (6)
(b) Using only your array (no re-scanning), list all proper borders of the full string P (a border is a proper prefix that is also a suffix). (3)
(c) A string of length has smallest period iff its longest border has length . Determine whether P has a period that divides its length, and state whether P is a repetition of a smaller block. (3)
Question 2 — Z-algorithm applied (12 marks)
Let T = "aabxaayaab".
(a) Compute the full Z-array for T. (Take to be undefined/0 by convention.) (6)
(b) You are given a pattern and text . Explain precisely how to use a single Z-array computation on the string (where # is a separator not in the alphabet) to find all occurrences of in , and state the exact condition on values that signals a match. (4)
(c) Using your rule from (b), find all occurrences of P = "aab" in X = "xaayaab". (2)
Question 3 — Rabin-Karp rolling hash (12 marks)
Use a polynomial rolling hash with base and modulus , mapping a, b, c. The hash of a window is
(a) Compute the hash of the pattern P = "cab". (3)
(b) For text T = "abcaba", compute the hash of the first window "abc", then use the rolling update to obtain the hash of the next window "bca". Show the rolling formula you use. (5)
(c) A spurious hit (hash collision without a true match) occurs at some window in T for pattern P. Identify it, if any, among all windows of length 3, and state how Rabin-Karp avoids reporting it as a real match. (4)
Question 4 — Manacher's algorithm (12 marks)
Consider S = "abacaba".
(a) List the length of the longest palindromic substring centred at each character (odd-length centres only). Present this as an array of radii. (6)
(b) State the longest palindromic substring of S and its length. (2)
(c) Manacher's algorithm reuses previously computed radii using a "current rightmost palindrome" . When expanding centre with mirror , give the exact initialisation of the radius at before attempting expansion, and explain why this initialisation is always valid. (4)
Question 5 — Multi-pattern & suffix structures (12 marks)
(a) You must search a text of length for patterns of total length . Compare the total worst-case time of (i) running KMP independently per pattern versus (ii) building one Aho-Corasick automaton. Give both complexities and state when (ii) is strictly better. (4)
(b) For the set of patterns {"he", "she", "his", "hers"}, describe the failure (suffix) link target of the trie node reached by the path "she", and explain what output(s) are reported when the automaton reaches that node while scanning text "ushers". (4)
(c) Given the string S = "banana" (append $ conceptually), list all 7 suffixes and give the suffix array (the sorted starting indices). Then give the LCP array value between the first two adjacent suffixes in sorted order. (4)
Answer keyMark scheme & solutions
Question 1 (12)
(a) P = "abacabab", indices 0–7.
Build = length of longest proper prefix that is also suffix of P[0..i].
| i | char | π |
|---|---|---|
| 0 | a | 0 |
| 1 | b | 0 |
| 2 | a | 1 |
| 3 | c | 0 |
| 4 | a | 1 |
| 5 | b | 2 |
| 6 | a | 3 |
| 7 | b | 2 |
Reasoning at i=6: prefix "abaca", matching "a","ab","aba" → π=3 (since P[3]... actually compare: previous π[5]=2, P[6]='a', P[2]='a' match → π=3). At i=7: from π=3, P[7]='b', P[3]='c' mismatch, fall back to π[2]=1, P[7]='b', P[1]='b' match → π=2. (6, ~1 per correct entry from i=2 onward)
(b) Borders of full P come from iterating on the last index (7): . So border lengths are 2 and 0 → the only non-empty proper border is "ab" (length 2). (3)
(c) Longest border length = 2, so smallest period . Since , P is not a whole-number repetition of a smaller block; it is not periodic in the "power of a block" sense. (3)
Question 2 (12)
(a) T = "aabxaayaab", indices 0–9.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| char | a | a | b | x | a | a | y | a | a | b |
| Z | – | 1 | 0 | 0 | 2 | 1 | 0 | 3 | 1 | 0 |
Checks: : T[1]='a'=T[0], T[2]='b'≠T[1]? compare prefix: T[0..]="aab", suffix at 1 "abx…" → match length 1. : "aay…" vs prefix "aab" → "aa" match, then 'y'≠'b' → 2. : suffix "aab" vs prefix "aab" → full 3. (6)
(b) Concatenate . Compute over . For every position in the -portion (i.e. ), if then occurs in starting at index . The separator # (not in alphabet) guarantees can never exceed , so signals a full match. (4)
(c) aab#xaayaab, . Positions in X region needing : the suffix "aab" at end matches → occurrence. Index in X: the "aab" starts at X-index 4 (x a a y a a b → positions 4,5,6). So one occurrence at index 4 of X. (2)
Question 3 (12)
(a) P="cab", : c=3,a=1,b=2. .
.
. . . : , .
. (3)
(b) Window "abc": =a1,b2,c3. . , . So .
Rolling to "bca": remove leading a1, add trailing a1: . , . So . . , (add ): . Verify directly: "bca" = b2,c3,a1 → ; : , . ✓ . (5)
(c) Pattern hash 88. Compute all length-3 window hashes of T="abcaba":
- "abc"→16
- "bca"→97
- "cab": c3,a1,b2 → ✓ hash = 88
- "aba": a1,b2,a1 → : , →14.
Window "cab" at index 2 has hash 88 = . Here the string "cab" actually equals P, so it is a true match, not spurious. Checking all windows, no window has hash 88 with a differing string → no spurious hit occurs for this text. Rabin-Karp always confirms a hash match with a direct character comparison, so any collision would be rejected; here the only hash match is genuine. (4) (Award full marks for correctly finding no spurious hit and stating the verification step.)
Question 4 (12)
(a) S="abacaba" (indices 0–6). Odd-centre palindrome radius (number of characters each side, so palindrome length = 2·radius+1):
| i | char | radius | pal length |
|---|---|---|---|
| 0 | a | 0 | 1 |
| 1 | b | 0 | 1 |
| 2 | a | 1 | 3 (aba) |
| 3 | c | 3 | 7 (abacaba) |
| 4 | a | 1 | 3 (aba) |
| 5 | b | 0 | 1 |
| 6 | a | 0 | 1 |
(6)
(b) Longest palindromic substring = "abacaba", length 7 (centre index 3). (2)
(c) Before expanding centre inside with mirror , initialise This is valid because the substring is a palindrome, so the neighbourhood of mirrors that of at least up to the boundary ; we can safely copy but must cap it at since beyond the mirror symmetry is not guaranteed and must be verified by explicit expansion. (4)
Question 5 (12)
(a) (i) KMP per pattern: preprocess each pattern and scan text once per pattern → total . (ii) Aho-Corasick: build automaton (over fixed alphabet) and single text scan → total . Aho-Corasick is strictly better when is large (many patterns), since it replaces the term by a single scan. (4)
(b) In the trie for {he, she, his, hers}, the node reached by "she" has a failure link to the node reached by "he" (the longest proper suffix of "she" that is a prefix of some pattern). Because "he" is itself a complete pattern, reaching the "she" node while scanning "ushers" reports both "she" (the node's own pattern) and "he" (via the output link following the failure link). At the position ending "...she" in "ushers", outputs = {she, he}. (4)
(c) Suffixes of S="banana":
0: banana
1: anana
2: nana
3: ana
4: na
5: a
6: (empty, $)
Sorted (with $ smallest): order of starting indices:
$(6), a(5), ana(3), anana(1), banana(0), na(4), nana(2).
Suffix array = [6, 5, 3, 1, 0, 4, 2] (or [5,3,1,0,4,2] if $ omitted).
First two adjacent real suffixes in sorted order: a(5) and ana(3). LCP = 1 (common prefix "a"). (4)
[
{"claim":"KMP pi array for abacabab ends with border length 2","code":"P='abacabab'\npi=[0]*len(P)\nk=0\nfor i in range(1,len(P)):\n while k>0 and P[i]!=P[k]:\n k=pi[k-1]\n if P[i]==P[k]:\n k+=1\n pi[i]=k\nresult = (pi==[0,0,1,0,1,2,3,2])"},
{"claim":"Hash of cab base31 mod101 is 88","code":"v={'a':1,'b':2,'c':3}\nb=31; q=101\nP='cab'\nH=0\nfor c in P:\n H=(H*b+v[c])%q\nresult = (H==88)"},
{"claim":"Rolling hash of bca equals 97","code":"v={'a':1,'b':2,'c':3}\nb=31;q=101\ndef h(s):\n H=0\n for c in s: H=(H*b+v[c])%q\n return H\nresult = (h('bca')==97 and h('abc')==16)"},
{"claim":"Suffix array of banana (no sentinel) is [5,3,1,0,4,2]","code":"S='banana'\nsuf=sorted(range(len(S)), key=lambda i:S[i:])\nresult = (suf==[5,3,1,0,4,2])"},
{"claim":"Manacher longest palindrome of abacaba has length 7","code":"S='abacaba'\nn=len(S)\nbest=0\nfor c in range(n):\n r=0\n while c-r>=0 and c+r<n and S[c-r]==S[c+r]:\n r+=1\n best=max(best,2*(r-1)+1)\nresult = (best==7)"}
]