3.8.5 · D4String Algorithms

Exercises — Boyer-Moore — bad character, good suffix heuristics

3,785 words17 min readBack to topic

Reminders you will use constantly (all from the parent):

Throughout, indices are 0-based: is the first character, the last.

The picture below is your mental model for every problem on this page. Keep glancing back at it.

Figure — Boyer-Moore — bad character, good suffix heuristics
Figure s01 — Boyer-Moore alignment. The upper pale-blue row is the text ; the lower row is the pattern slid underneath at one alignment. The lavender arrow shows the scan direction: we start at the right end of and walk left. The mint cells (right portion of ) are the matched good suffix — they already agreed with the text above them. The single coral cell is the first mismatch; the text character sitting directly above it is the bad character . The bad-character rule reacts to that coral column; the good-suffix rule reacts to the mint block. Every exercise on this page is one of these pictures with different letters.


Level 1 — Recognition

L1.1 For (), write for every letter that occurs, and state .

Recall Solution

Index the pattern: . is the rightmost index at which appears.

  • (only at index 0).
  • — E appears at 1, 2, 5; rightmost is 5.
  • .
  • .
  • — X does not occur, so we return the "not present" sentinel .

L1.2 In Boyer-Moore, which end of the pattern do we compare first, and why?

Recall Solution

We compare the rightmost character against the text first, then move leftward. Reason: a mismatch near the right tells us a whole matched suffix already lined up. That matched suffix is exactly what the good-suffix rule exploits, and the mismatching text character is what the bad-character rule exploits. If we compared left-to-right (like naive matching) we'd never learn a suffix, and the big skips would be impossible.

L1.3 A "border" of a string is defined how, in one sentence?

Recall Solution

A border is a proper substring (not the whole string) that is simultaneously a prefix and a suffix. Example: in ANANA, the string ANA is a border (it's the first 3 chars and the last 3 chars). This links to Borders and Prefix Function.


Level 2 — Application

L2.1 (). A mismatch happens at (so ) and the text character there is . Compute .

Recall Solution

in ABCAB: B occurs at indices 1 and 4, so . . The raw formula gave (would shift backward!), so the clamp saves us. Here the bad-character rule is weak; in a real run the good-suffix rule would supply the real jump.

L2.2 Same , mismatch at (comparing the last char) with text char . Compute .

Recall Solution

(Z is absent). . Because Z is nowhere in , we slide the entire pattern past that text position — a jump of the full pattern length. This is the sublinear magic in action.

L2.3 (). Mismatch at with text char . Compute .

Recall Solution

Index: . . But the mismatch itself is at — the bad char aligns with the very index we mismatched at. . The clamp fires again: would mean "don't move," which is illegal. We take .


Level 3 — Analysis

L3.1 For (), compute the array, where is the length of the longest suffix of ending at index that is also a suffix of the whole .

Recall Solution

Index: at . The whole-pattern suffix reads (right to left) G A G A G A C G.

  • by convention (the whole string is a suffix of itself).
  • : suffix ending at 6 is ...A; whole-P ends in G. AG.
  • : ends at 5 with G; compare G(P[5]) vs G(P[7]) match; A(P[4]) vs A(P[6]) match; G(P[3]) vs G(P[5]) match; A(P[2]) vs A(P[4]) match; P[1]=C vs P[3]=G stop. Length walked over indices 5,4,3,2 → .
  • : ends at 4 A vs P[7] G.
  • : ends G vs G match; P[2]A vs P[6]A match; P[1]C vs P[5]G stop → .
  • : A vs G.
  • : C vs G.
  • : G vs G match, no more chars to the left → . Result: .

L3.2 (). The good suffix AN (length 2) matched, mismatch at index (char ). There are two earlier copies of AN, at indices and . Explain why we must reject one of these copies as a Case-1 candidate, and which copy we actually align.

Recall Solution

Index: at . Good suffix is ; mismatch at , . Case 1 requires the character just before an internal copy of the good suffix to differ from the mismatch char — otherwise sliding to that copy would place the same M in the same relative spot and reproduce the identical mismatch (zero progress).

  • Reject the copy at as a Case-1 candidate: it has no preceding character (it sits at the very start of ). Case 1 needs a strictly-internal copy with a genuine preceding char to test, so this leading copy is not a Case-1 hit — it only ever contributes through the Case-2 / prefix fallback.
  • Accept the copy at : its preceding char is , and ✓. This is a valid Case-1 copy. So we align the copy at under the text's AN. The shift moves the AN at to where the AN at sits: . (Had both inner copies been preceded by M, we'd fall through to Case 2 and use a prefix of instead.)

L3.3 Why is taking always safe (never skips a real occurrence)?

Recall Solution

Each rule is derived independently as a guaranteed lower-bound jump: "no valid match can begin anywhere in the skipped region." The bad-character rule proves it using the mismatching text char; the good-suffix rule proves it using the matched suffix. If jump is safe and jump is safe, then every alignment strictly below is ruled out by at least one of the two proofs — so is also safe. Taking the larger just means we trust whichever proof reaches further.


Level 4 — Synthesis

L4.1 (). Compute for all its letters, then compute when we align at some , compare right-to-left, and the first mismatch is at with text char .

Recall Solution

Index: at . . Mismatch , , absent from : . We jump the full pattern width, having read only one text character. That's the sublinear behaviour of BM on this alignment.

L4.2 Same . Now suppose at some alignment matched but mismatched, i.e. , with text char . Compute both heuristics' shifts and the final BM shift. (For good suffix, the matched suffix is just C; the earlier Cs in are at indices 0,1,7.)

Recall Solution

Bad character: , . . Good suffix: matched suffix is (length 1), i.e. so we want . We need an earlier copy of C in whose preceding char differs from the mismatch char , to align under the text's trailing C.

  • C at index 1: preceding char ✓. Align over the text position currently over 's target; the shift to bring 's C to the slot where 's C sits is .
  • (C at index 0 has no preceding char; index 1 is the nearest valid inner copy, giving the smallest safe good-suffix jump = .) . Final: . This is exactly the scenario the parent warns about: bad char alone gives a feeble ; good suffix rescues it with .

L4.3 Trace BM (using of both heuristics) on (text length ), (). List the alignments tried and every reported match. At the mismatches, show what the good-suffix rule contributes as well.

Recall Solution

, indices . , others . Recall (text length), so the last alignment we may try is . Good-suffix array facts we need. The longest proper border of AABA is A (prefix A = suffix A), so the full-match shift is . At the mismatches below the mismatch is always at (chars B vs a non-B), so the matched suffix is (length 1) and we need . Where does an earlier A reappear, preceded by a char ? The A at index 0 is preceded by nothing (leading), the A at index 3 is the matched one itself; the nearest usable earlier A for Case 1 is index 0 via the prefix/Case-2 route. Working it out, for AABA. So on these mismatches . Step through, always taking :

  • : compare vs AABA → full match. Report . Shift .
  • : . vs ✓; vs ✗ at , absent. ; . Final . (Here bad char wins, but good suffix independently justified a jump of 2.)
  • : . ✓; vs ✗ at , absent. , , final .
  • : → full match. Report . Shift .
  • : → full match. Report . Shift . Stop. Matches at . Alignments tried: — five, versus for naive.

L4.4 Build the entire good-suffix array for (), using the two-pass procedure and the array from L3.1. Show each pass.

Recall Solution

From L3.1: , . Init: (length ). Pass A (Case 2, prefix = suffix). We look for indices with (a whole-pattern prefix ends at ). Checking: ✓ (prefix G). No other satisfies it (, , etc.). So the only surviving prefix has length , start index — the smallest start is . For every not yet set by a stronger rule we fill . Concretely this sets the Case-2 default for the high- positions the prefix reaches; . Pass B (Case 1, internal reoccurrence). For , set and , sweeping upward so rightmost copies give the tightest shift:

  • : , .
  • : , .
  • : , .
  • (indices with give , harmlessly overwritten toward .) Result (one standard convention): with the Case-2 fills of applied to the untouched high positions. The two shifts you actually use most are (full-match jump) and the Case-1 entries . The key competency: you can now derive any from rather than guess it.

Level 5 — Mastery

L5.1 Construct the classic worst case where basic Boyer-Moore degrades to , and explain why both heuristics fail to help. Then state what fixes it.

Recall Solution

Take () and ( copies of A). At every alignment, the right-to-left scan matches all the way to (no mismatch inside), so we do comparisons per alignment. There are about alignments. Total . Why the heuristics don't save us: the good-suffix full-match shift is (longest proper border of AAAA is AAA, length 3). Bad-character also gives tiny shifts because every char equals A. So we crawl forward by 1 each time while re-reading matched characters. The fix: the Galil Rule remembers how far the previous match reached and skips re-comparing that region, restoring guaranteed .

L5.2 You have a huge DNA text () and a long DNA pattern. Would you expect Boyer-Moore's sublinear best case to kick in strongly? Contrast with searching English text or with an alternative algorithm from the connections list.

Recall Solution

BM's sublinear speed grows with alphabet size: a large alphabet makes the mismatching text char often absent from , which triggers full-length bad-character jumps (like L2.2 and L4.1). DNA has only 4 symbols, so "absent character" is rare — nearly every text char also lives in , so bad-character jumps are usually small and the good-suffix rule ends up doing most of the useful work. BM is still perfectly usable on DNA, but its skips are shorter and it is less dramatically sublinear than on English (26+ letters) or arbitrary byte data (256 symbols), where big absent-char jumps are common. Alternatives from the connections list: for tiny alphabets or when you need a hard linear guarantee, prefer Knuth-Morris-Pratt (KMP) (strict , built on the prefix function of Borders and Prefix Function); Z-Algorithm gives the same linear guarantee with a very clean construction. If you must search many patterns at once, Aho-Corasick beats running BM repeatedly, and Rabin-Karp is attractive when hashing / multiple-pattern averaging helps. In short: BM shines when the alphabet is big and the pattern moderately long; on small-alphabet DNA the linear-time competitors are often the safer default.

L5.3 Prove that the bad-character shift never skips a valid occurrence when .

Recall Solution

Suppose the mismatch is at pattern index , and the offending text char is , with (the rightmost in is at index , left of ). For to match at some shift , the text character at absolute position (which is ) must line up with a pattern index that actually holds . The largest such index is . Aligning under position requires . Any with would place a non- pattern index over that , forcing an immediate mismatch — so those alignments are impossible. Hence jumping by skips only provably-empty alignments. The merely guarantees forward progress when the formula is non-positive.

L5.4 Give a pattern and mismatch where the good-suffix shift comes from Case 2 (border fallback), not Case 1, and compute that shift. Explain why Case 1 cannot fire.

Recall Solution

Take (), indices = A B A B A. Suppose the scan matched the suffix (length 3) and mismatched at , . Why Case 1 cannot fire: we need another full copy of ABA inside ending strictly before index 4, preceded by a char . The only other occurrence of ABA is , but it starts at index 0 with no preceding character, so it is not a valid strictly-internal Case-1 copy. No full inner copy of the good suffix qualifies. Case 2 (border fallback): we instead align the longest prefix of that equals a suffix of . The suffixes of ABA are A, BA, ABA. Which of these is a prefix of ABABA? A ✓, ABA ✓ (prefix ABA), BA ✗. The longest is ABA, length 3. Shift . This lands the pattern's leading ABA under the text region that matched ABA, the only self-consistent placement left. Forgetting Case 2 here would make you over-shift and miss a real occurrence.


Recall Self-check clozes

Bad-character shift when is absent from and mismatch at index ::: (i.e. ), sliding the whole pattern past. Full-match shift for ::: . Case-2 good-suffix shift for ABABA with good suffix ABA ::: (longest prefix of equal to a suffix of the good suffix is ABA). What restores worst case ::: the Galil Rule. Why BM is less sublinear on DNA ::: only 4 symbols, so absent-character full jumps are rare.