Introduction
What to say
- The Knuth–Morris–Pratt algorithm finds all occurrences of a pattern in a text in O(n + m) — n is the length of the text, m of the pattern.
- Its key is the prefix function, which lets it search without redundant comparisons.
KMP in one sentence
Find every occurrence of a pattern in a text in O(n + m), using one precomputed array — the prefix function.
Prefix Function
What to say
- π[i] is the length of the longest proper prefix of s[0…i] that is also a suffix of s[0…i]. Proper means not the whole substring.
- Do example 1 by the definition on the board — list the prefixes and suffixes for every i. Then examples 2 and 3 quickly.
- The naive computation tries every length at every position: O(n³).
- The efficient one starts from the previous answer j = π[i−1]. If s[i] == s[j], the answer is j + 1; otherwise fall back to π[j−1] and try again.
- Trace "abcabcd" step by step, then let the demo step through other strings.
Definition
that is also a suffix of s[0…i]
Proper prefix — a prefix that is not equal to the entire substring.
Example 1 · s = "abcabcd"
i = 0: s[0...0] = "a"
Proper prefixes: (none)
Suffixes: (none)
π[0] = 0
i = 1: s[0...1] = "ab"
Proper prefixes: "a"
Suffixes: "b"
No match → π[1] = 0
i = 2: s[0...2] = "abc"
Proper prefixes: "a", "ab"
Suffixes: "c", "bc"
No match → π[2] = 0
i = 3: s[0...3] = "abca"
Proper prefixes: "a", "ab", "abc"
Suffixes: "a", "ca", "bca"
Match: "a" → π[3] = 1
i = 4: s[0...4] = "abcab"
Proper prefixes: "a", "ab", "abc", "abca"
Suffixes: "b", "ab", "cab", "bcab"
Match: "ab" → π[4] = 2
i = 5: s[0...5] = "abcabc"
Proper prefixes: "a", "ab", "abc", "abca", "abcab"
Suffixes: "c", "bc", "abc", "cabc", "bcabc"
Match: "abc" → π[5] = 3
i = 6: s[0...6] = "abcabcd"
Proper prefixes: "a", "ab", "abc", "abca", "abcab", "abcabc"
Suffixes: "d", "cd", "bcd", "abcd", "cabcd", "bcabcd"
No match → π[6] = 0
Result: π = [0, 0, 0, 1, 2, 3, 0]Example 2 · s = "aabaaab"
i = 0: π[0] = 0 i = 1: "aa" → prefix "a" = suffix "a" → π[1] = 1 i = 2: "aab" → no match → π[2] = 0 i = 3: "aaba" → prefix "a" = suffix "a" → π[3] = 1 i = 4: "aabaa" → prefix "aa" = suffix "aa" → π[4] = 2 i = 5: "aabaaa" → prefix "aa" = suffix "aa" → π[5] = 2 i = 6: "aabaaab" → prefix "aab" = suffix "aab" → π[6] = 3 Result: π = [0, 1, 0, 1, 2, 2, 3]
Example 3 · s = "aaaa"
i = 0: π[0] = 0 i = 1: "aa" → prefix "a" = suffix "a" → π[1] = 1 i = 2: "aaa" → prefix "aa" = suffix "aa" → π[2] = 2 i = 3: "aaaa" → prefix "aaa" = suffix "aaa" → π[3] = 3 Result: π = [0, 1, 2, 3]
Naive approach — O(n³)
#include <iostream> #include <vector> using namespace std; vector<int> prefix_function_naive(string s) { int n = s.size(); vector<int> pi(n); pi[0] = 0; // By definition for (int i = 1; i < n; i++) { // Try all possible lengths from longest to shortest for (int len = i; len > 0; len--) { // Check if prefix of length len equals suffix of length len bool match = true; for (int j = 0; j < len; j++) { if (s[j] != s[i - len + 1 + j]) { match = false; break; } } if (match) { pi[i] = len; break; } } } return pi; } int main() { string s = "abcabcd"; vector<int> pi = prefix_function_naive(s); cout << "String: " << s << endl; cout << "Prefix function: "; for (int i = 0; i < pi.size(); i++) cout << pi[i] << " "; cout << endl; return 0; }
String: abcabcd Prefix function: 0 0 0 1 2 3 0
Another version · naive with substr
The same brute force, written with substr: every length from 1 to i, and the last match wins.
the full program — substr25 lines
#include <iostream> #include <vector> using namespace std; vector<int> prefix_function(string s) { int n = s.size(); vector<int> p(n); p[0] = 0; for (int i = 1; i < n; i++) { for (int len = 1; len <= i; len++) if (s.substr(0, len) == s.substr(i - len + 1, len)) p[i] = len; } return p; } int main() { string s; cin >> s; vector<int> p = prefix_function(s); for (int i = 0; i < s.size(); i++) cout << p[i] << " "; return 0; }
abcabcd
0 0 0 1 2 3 0
Efficient approach — O(n)
- Start with π[0] = 0
- For each position i, look at the previous match length j = π[i−1]
- If s[i] == s[j], then π[i] = j + 1
- If s[i] != s[j], fall back to j = π[j−1] and try again
- Continue falling back until a match is found or j reaches 0
#include <iostream> #include <vector> using namespace std; vector<int> prefix_function(string s) { int n = s.size(); vector<int> pi(n); pi[0] = 0; // First element is always 0 for (int i = 1; i < n; i++) { int j = pi[i - 1]; // Previous prefix length // Fall back until we find a match or reach 0 while (j > 0 && s[i] != s[j]) { j = pi[j - 1]; } // If characters match, increment the length if (s[i] == s[j]) { j++; } pi[i] = j; } return pi; } int main() { string s; cout << "Enter string: "; cin >> s; vector<int> pi = prefix_function(s); cout << "\nString: "; for (int i = 0; i < s.size(); i++) cout << s[i] << " "; cout << endl; cout << "Index: "; for (int i = 0; i < s.size(); i++) cout << i << " "; cout << endl; cout << "Prefix function: "; for (int i = 0; i < pi.size(); i++) cout << pi[i] << " "; cout << endl; return 0; }
abcabcd
Enter string: String: a b c a b c d Index: 0 1 2 3 4 5 6 Prefix function: 0 0 0 1 2 3 0
Time: O(n) · Space: O(n)
Step-by-step trace · s = "abcabcd"
i = 0: pi[0] = 0 (by definition) i = 1: j = pi[0] = 0 s[1] = 'b', s[0] = 'a' → no match pi[1] = 0 i = 2: j = pi[1] = 0 s[2] = 'c', s[0] = 'a' → no match pi[2] = 0 i = 3: j = pi[2] = 0 s[3] = 'a', s[0] = 'a' → match! pi[3] = 1 i = 4: j = pi[3] = 1 s[4] = 'b', s[1] = 'b' → match! pi[4] = 2 i = 5: j = pi[4] = 2 s[5] = 'c', s[2] = 'c' → match! pi[5] = 3 i = 6: j = pi[5] = 3 s[6] = 'd', s[3] = 'a' → no match Fall back: j = pi[2] = 0 s[6] = 'd', s[0] = 'a' → no match pi[6] = 0 Result: π = [0, 0, 0, 1, 2, 3, 0]
Another version · both versions in one file
The brute force and the fast version side by side; main uses the fast one.
the full program — two versions42 lines
// abababaa #include <iostream> #include <vector> using namespace std; vector<int> prefix_function(string s) { int n = s.size(); vector<int> pi(n); pi[0] = 0; for (int i = 1; i < n; i++) { for (int l = 1; l <= i; l++) { if (s.substr(0, l) == s.substr(i - l + 1, l)) pi[i] = l; } } return pi; } vector<int> prefix_functionv2(string s) { int n = s.size(); vector<int> pi(n); pi[0] = 0; for (int i = 1; i < n; i++) { int j = pi[i - 1]; while (j > 0 && s[i] != s[j]) j = pi[j - 1]; if (s[i] == s[j]) j++; pi[i] = j; } return pi; } int main() { string s; cin >> s; vector<int> pi = prefix_functionv2(s); for (int i = 0; i < s.size(); i++) cout << pi[i] << " "; return 0; }
abababaa
0 0 1 2 3 4 5 1
Demo · the prefix function, step by step
Knuth-Morris-Pratt (KMP) Algorithm
What to say
- Find all occurrences of a pattern in a text. Glue them:
pattern + "#" + text, where # appears in neither. - Compute π of the combined string. Wherever π[i] equals the pattern length, the pattern ends at i — it starts in the text at i − 2m.
- The separator stops a match from running across the border between the pattern and the text.
- Work the first example on the board column by column, then the second one with overlapping occurrences.
Problem and idea
Given a text (length n) and a pattern (length m), find all occurrences of the pattern in the text.
- Compute the prefix function of the combined string
- When π[i] equals the pattern length, a match is found
- The separator keeps the pattern from matching across the boundary
KMP implementation
#include <iostream> #include <vector> using namespace std; vector<int> prefix_function(string s) { int n = s.size(); vector<int> pi(n); pi[0] = 0; for (int i = 1; i < n; i++) { int j = pi[i - 1]; while (j > 0 && s[i] != s[j]) { j = pi[j - 1]; } if (s[i] == s[j]) { j++; } pi[i] = j; } return pi; } void KMP(string text, string pattern) { // Create combined string: pattern + '#' + text string s = pattern + "#" + text; int n = s.size(); int m = pattern.size(); // Compute prefix function vector<int> pi = prefix_function(s); // Find all occurrences for (int i = m + 1; i < n; i++) { // If prefix length equals pattern length, we found a match if (pi[i] == m) { // Position in text = i - m - m int pos = i - 2 * m; cout << "Pattern found at index " << pos << endl; } } } int main() { string text, pattern; cout << "Enter text: "; cin >> text; cout << "Enter pattern: "; cin >> pattern; cout << "\nSearching for pattern in text...\n" << endl; KMP(text, pattern); return 0; }
ababcabcabababd ababd
Enter text: Enter pattern: Searching for pattern in text... Pattern found at index 10
Example · text = "ababcabcabababd", pattern = "ababd"
s = "ababd#ababcabcabababd"
Index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 String: a b a b d # a b a b c a b c a b a b a b d π: 0 0 1 2 0 0 1 2 3 4 0 1 2 0 1 2 3 4 3 4 5
π[20] = 5: position in text = 20 - 2×5 = 10
text = "ababcabcabababd"
index 012345678901234
^^^^^ (index 10–14: "ababd")Another example · text = "aabaacaabaa", pattern = "aaba"
s = "aaba#aabaacaabaa"
Index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 String: a a b a # a a b a a c a a b a a π: 0 1 0 1 0 1 2 3 4 2 0 1 2 3 4 2
π[8] = 4 → position = 8 - 2×4 = 0 π[14] = 4 → position = 14 - 2×4 = 6
text = "aabaacaabaa"
index 01234567890
^^^^ (index 0–3: "aaba")
^^^^ (index 6–9: "aaba")Time complexity
| Operation | Time |
|---|---|
| Create the combined string | O(n + m) |
| Compute the prefix function | O(n + m) |
| Find matches | O(n + m) |
| Total | O(n + m) |
Problems Solved Using Prefix Function and KMP
What to say
- One array, eight problems. Each of them is "build the right string, compute π, read one or a few values".
- The period: if π[n−1] = k, the first k characters equal the last k, the shortest period has length n − k, and the string is periodic when n is divisible by it. Use the demo here.
- Borders: π[n−1] is the longest one; jump k = π[k−1] to get every shorter one.
1 · Pattern matching
Find all occurrences of a pattern: create pattern + '#' + text and compute the prefix function.
void find_all_occurrences(string text, string pattern) { string s = pattern + "#" + text; vector<int> pi = prefix_function(s); int m = pattern.size(); cout << "Pattern found at positions: "; for (int i = m + 1; i < s.size(); i++) { if (pi[i] == m) { cout << (i - 2 * m) << " "; } } cout << endl; }
2 · Counting occurrences, overlapping included
Count how many times π[i] equals the pattern length.
int count_occurrences(string text, string pattern) { string s = pattern + "#" + text; vector<int> pi = prefix_function(s); int m = pattern.size(); int count = 0; for (int i = m + 1; i < s.size(); i++) { if (pi[i] == m) { count++; } } return count; } // Example: text = "aaaa", pattern = "aa" // s = "aa#aaaa" // Occurrences at: 0, 1, 2 → count = 3
text = "aaaa", pattern = "aa" s = "aa#aaaa" Occurrences at: 0, 1, 2 → count = 3
3 · Shortest period of a string
"abcabcabc" has period "abc" — length 3.
int shortest_period(string s) { int n = s.size(); vector<int> pi = prefix_function(s); int len = n - pi[n - 1]; // Length of shortest period // Check if string is actually periodic if (n % len == 0) { return len; } else { return n; // No period (entire string) } } // Example: s = "abcabcabc" // pi[8] = 6 (prefix "abcabc" = suffix "abcabc") // len = 9 - 6 = 3 // 9 % 3 == 0 → period length is 3
s = "abcabcabc" pi[8] = 6 (prefix "abcabc" = suffix "abcabc") len = 9 - 6 = 3 9 % 3 == 0 → period length is 3
- If π[n−1] = k, the first k characters match the last k
- The shortest period has length n − k
- If n is divisible by n − k, the string is periodic
4 · String compression
Can the string be written as a repeated pattern? Check whether the shortest period divides its length.
string compress(string s) {
int n = s.size();
int period_len = shortest_period(s);
if (period_len < n) {
int count = n / period_len;
string pattern = s.substr(0, period_len);
return pattern + " × " + to_string(count);
}
return s; // Cannot compress
}
// Example: "abcabcabc" → "abc × 3"
// Example: "abcdef" → "abcdef" (no compression)"abcabcabc" → "abc × 3" "abcdef" → "abcdef" (period 6 = whole string, no compression)
5 · Pattern in a cyclic string
Does the pattern occur in some rotation of the text? Search in text + text.
bool pattern_in_rotation(string text, string pattern) { string doubled = text + text; string s = pattern + "#" + doubled; vector<int> pi = prefix_function(s); int m = pattern.size(); for (int i = m + 1; i < s.size(); i++) { if (pi[i] == m) { return true; } } return false; } // Example: text = "abcde", pattern = "dea" // s = "dea#abcdeabcde" // Pattern "dea" found → true
text = "abcde", pattern = "dea" s = "dea#abcdeabcde" "dea" found at index 3 of the doubled text → true
6 · Longest palindromic prefix
Compute the prefix function of s + '#' + reverse(s); the last value is the answer.
int longest_palindrome_prefix(string s) { string rev = s; reverse(rev.begin(), rev.end()); string combined = s + "#" + rev; vector<int> pi = prefix_function(combined); return pi[combined.size() - 1]; } // Example: s = "abacaba" // rev = "abacaba" // combined = "abacaba#abacaba" // π[14] = 7 → entire string is palindrome
s = "abacaba", rev = "abacaba" combined = "abacaba#abacaba" π[14] = 7 → the entire string is a palindrome
7 · All borders
All lengths k for which the prefix of length k equals the suffix of length k: start from π[n−1] and keep jumping back.
vector<int> find_all_borders(string s) { int n = s.size(); vector<int> pi = prefix_function(s); vector<int> borders; int k = pi[n - 1]; while (k > 0) { borders.push_back(k); k = pi[k - 1]; } reverse(borders.begin(), borders.end()); return borders; } // Example: s = "abcabcab" // Borders: lengths 2, 5 ("ab", "abcab")
s = "abcabcab"
Borders: lengths 2, 5 ("ab", "abcab")8 · Several patterns
For each pattern, build its own pattern + '#' + text.
void find_multiple_patterns(string text, vector<string> patterns) { for (int k = 0; k < patterns.size(); k++) { string s = patterns[k] + "#" + text; vector<int> pi = prefix_function(s); int m = patterns[k].size(); for (int i = m + 1; i < s.size(); i++) { if (pi[i] == m) { cout << "Pattern " << k << " (\"" << patterns[k]; cout << "\") found at index " << (i - 2 * m) << endl; break; // Found first occurrence } } } }
Demo · period and borders
Comparison: KMP vs Rabin-Karp
Side by side
| Feature | KMP | Rabin–Karp |
|---|---|---|
| Time complexity | O(n + m) guaranteed | O(n + m) average, O(n × m) worst |
| Space | O(n + m) | O(n) for prefix hashes |
| False positives | none | possible (hash collisions) |
| Implementation | prefix function | hash function |
| Multiple patterns | need modification | easy to extend |
| Preprocessing | prefix function | hash computation |
When to use which
KMP
- Guaranteed O(n + m) time is needed
- No false positives are acceptable
- A single pattern
Rabin–Karp
- Multiple patterns
- A simpler implementation is preferred
- A small probability of collision is acceptable
Advantages and Disadvantages
Advantages
- Guaranteed O(n + m) time — no worst-case degradation
- No false positives — always correct
- Simple, elegant approach — using
pattern + '#' + text - The prefix function is versatile — it solves many problems
- No backtracking — one linear scan
Disadvantages
- Extra space — O(n + m) for the combined string
- Preprocessing required — compute the prefix function
- Very long texts — the whole combined string has to be stored
- Single pattern — repeat the work for each pattern
Common Questions
Q1 · Why do we need the separator '#'?
Without it the pattern could match across the boundary between the pattern and the text, giving wrong results.
pattern = "ab", text = "cab" Without #: "abcab" → would match at position 0 (wrong!) With #: "ab#cab" → no match (correct!)
In "abcab", π[4] = 2 = m, so the formula reports position 4 − 2×2 = 0 — but in "cab" the pattern starts at index 1.
Q2 · What if '#' appears in the text or the pattern?
Choose a separator that appears in neither:
'#'if only letters are used'$'for printable ASCII- any character with a code larger than every character of the alphabet
Q3 · Why is this simpler than traditional KMP?
Traditional KMP moves a pointer in the pattern and a pointer in the text and has to handle both carefully. The combined string reduces it to one loop that checks π[i] == m.
Q4 · Can we save space?
Yes — the prefix function can be computed on the fly without storing the whole combined string, but the code becomes more complex.
Q5 · Why is the prefix function O(n)?
There is a while loop inside the for loop, but j grows by at most 1 per iteration — at most n times in total — so it can also decrease at most n times in total. Overall: O(n).
Key Takeaways
Eight things to remember
- The prefix function is the key — the longest proper prefix that is also a suffix
- Pattern matching with
pattern + '#' + text— elegant and simple - O(n + m) guaranteed — no worst-case degradation
- When π[i] = m, the pattern is found — at position i − 2m
- The separator '#' is essential — it prevents false matches
- The prefix function is O(n)
- Many applications — periods, compression, palindromes
- No false positives — unlike hashing
Practice Problems
Try these
- Implement KMP to find all occurrences of a pattern in a text
- Find the shortest period of a string
- Count overlapping pattern occurrences
- Check if a string is a concatenation of a repeated substring
- Find the longest palindromic prefix using the prefix function
- Find if a pattern exists in any rotation of a string
- Compress a string using a repeating pattern
- Find all borders of a string
- Count how many times each pattern appears in a text
- Find the lexicographically smallest rotation of a string
Lab 9 — the problems
What to say
- F is the plain KMP search and should be done first — it is the template, and everything else reuses it.
- G is the period formula, literally
n − π[n−1]. With the array the problem is one line; without it, it is impossible. - C is the rotation trick, B is occurrence counting, D is the longest overlap between a suffix and a prefix.
- E and H are the heavy pair. E mixes the prefix function with arithmetic that overflows int at 5·10¹¹; H runs on a string of 5·10⁶ characters.
- A is the awkward one to read: repeat A until B fits inside it. The answer is bounded by ⌈|B|/|A|⌉ + 1 — derive that bound with them, or they will loop forever.