Lecture 42: KMP Algorithm (Part 1) — Longest Prefix Suffix (LPS)
The Knuth-Morris-Pratt (KMP) algorithm is widely recognized as one of the most efficient string-matching algorithms in computer science. Before mastering the full KMP search procedure, it is essential to understand its foundational component: the Longest Prefix Suffix (LPS) array. These study notes detail the problem statement, naive approaches, structural bottlenecks, the optimal fallback intuition, step-by-step dry runs, and the production C++ implementation.
1. Motivation & Problem Definition: Longest Prefix Suffix (LPS)
In string manipulation tasks, we frequently need to identify repeating structural patterns within a single string. The Longest Prefix Suffix (LPS) problem asks us to find the length of the longest substring that acts as both a prefix and a suffix for a given string .
Problem Statement
Given a string s of length , calculate the length of the longest proper prefix that is also a proper suffix.
Example:
Input : s = "aacaa"
Output : 2
Explanation:
- Prefixes of "aacaa": "a", "aa", "aac", "aaca"
- Suffixes of "aacaa": "a", "aa", "caa", "acaa"
- Matching pairs : "a" (length 1), "aa" (length 2)
- Longest match : "aa" with length = 2
Note: A matching prefix and suffix are allowed to overlap in character positions within the original string.
2. The Rules of Prefix and Suffix (The Proper Constraint)
To correctly evaluate the LPS problem, two fundamental rules must be satisfied:
-
The "Proper" Constraint (Length ):
- A prefix cannot include the entire string .
- A suffix cannot include the entire string .
- If taking the whole string were permitted, every string of length would trivially have an answer of . Hence, .
-
Character Overlap Allowed:
- The characters constituting the prefix and suffix can share identical index positions in the middle of the string.
String: " a a a a a "
Index: 0 1 2 3 4
Prefix ("aaaa", indices 0..3): a a a a
Suffix ("aaaa", indices 1..4): a a a a
^ ^ ^ (Overlapping indices 1, 2, 3)
3. Naive Brute-Force Approach (All Substrings Comparison)
The most intuitive way to solve this problem is to explicitly generate all possible proper prefixes and proper suffixes, store them, and compare them in decreasing order of length.
Workflow
Complexity Breakdown
- Prefix/Suffix Generation: Generating all prefixes and suffixes takes time.
- String Comparisons: Comparing two strings of length takes time. Comparing up to pairs takes time.
- Overall Time Complexity: to depending on string slice optimizations.
- Overall Space Complexity: memory required to store generated substrings.
4. Two-Pointer Naive Approach & Why It Fails ( Breakdown)
To avoid allocating extra memory for substring storage, we can use a two-pointer technique. We fix a prefix pointer at index 0 and scan for matching starting characters using a suffix pointer.
Execution Flow on s = "aaaaad"
Structural Flaw
When a mismatch occurs at index ('d'), the naive approach completely forgets all previously matched characters. It resets the prefix pointer to 0 and restarts the suffix pointer from the next adjacent index ().
In repetitive patterns like "AAAA...AAD", this forces quadratic comparisons, causing the time complexity to degrade to .
5. Core Mental Model: Building the LPS Array Intuition
To achieve an solution, KMP constructs an auxiliary array called LPS of size .
Meaning of LPS[i]
LPS[i]stores the length of the longest proper prefix ofs[0...i]that is also a suffix ofs[0...i].
Conceptually, LPS[i] serves a dual purpose:
- It represents the length of the longest matching prefix-suffix ending at index
i. - It represents the index of the next character in the prefix that needs to be matched.
LPS Construction Trace for s = "abacabad"
6. The Key Optimization: Handling Mismatches via Fallback (LPS[prefix - 1])
The critical insight of the KMP algorithm lies in how it handles mismatches without backtracking the suffix pointer.
The Fallback Mechanic
When s[prefix] != s[suffix]:
- Do NOT increment
suffix. - Do NOT reset
prefixto0(unlessprefix == 0). - Fallback Rule: Set
prefix = LPS[prefix - 1].
Why prefix = LPS[prefix - 1] Works
Because the substring s[0 ... prefix - 1] was already successfully matched against the suffix ending just before suffix, we know its internal prefix-suffix structure from our previously calculated LPS values!
Instead of re-checking characters from index 0, we jump prefix back to LPS[prefix - 1]. This reuses our prior knowledge of matching sub-patterns and guarantees that we never backtrack the suffix pointer.
7. Step-by-Step Flow Walkthrough & Dry Run
Let's execute the algorithm step-by-step on s = "abacaba".
Algorithmic Rules Summary
LPS[0] = 0always.- Initialize
prefix = 0,suffix = 1. - Case 1:
s[prefix] == s[suffix]LPS[suffix] = prefix + 1prefix++suffix++
- Case 2:
s[prefix] != s[suffix]- Subcase 2A:
prefix == 0LPS[suffix] = 0suffix++
- Subcase 2B:
prefix > 0prefix = LPS[prefix - 1](Holdsuffixfixed!)
- Subcase 2A:
Detailed State Execution Table
8. Code Evolution: Decoupled & Production C++ Implementation
Naive vs. Optimal Fallback Control Flow
Complete Production C++ Code
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
/**
* Computes the Longest Prefix Suffix (LPS) array for a given string.
* @param s Input string.
* @return Vector of integers representing the LPS values at each index.
*/
vector<int> computeLPS(const string& s) {
int n = s.size();
vector<int> lps(n, 0);
int prefix = 0; // Points to the end of current matching prefix
int suffix = 1; // Scans through the string as suffix boundary
while (suffix < n) {
if (s[prefix] == s[suffix]) {
// CASE 1: Match found
lps[suffix] = prefix + 1;
prefix++;
suffix++;
} else {
// CASE 2: Mismatch occurs
if (prefix == 0) {
// Subcase 2A: No matching prefix prefix available
lps[suffix] = 0;
suffix++;
} else {
// Subcase 2B: Fallback prefix to previous longest matching prefix
prefix = lps[prefix - 1];
}
}
}
return lps;
}
};
int main() {
Solution sol;
string inputStr = "abacaba";
vector<int> lps = sol.computeLPS(inputStr);
cout << "String: " << inputStr << "\nLPS Array: [ ";
for (int val : lps) {
cout << val << " ";
}
cout << "]" << endl;
return 0;
}
9. Complexity Analysis & Edge Cases
Time Complexity:
- The
suffixpointer increments at most times. - The
prefixpointer is incremented at most times (once persuffixmatch). - The fallback statement
prefix = LPS[prefix - 1]strictly decreasesprefix. Sinceprefixcan only decrease as many times as it was incremented, the total number of fallbacks across the entire run is bounded by . - Amortized Time Complexity: .
Space Complexity:
- Requires an auxiliary vector of size to store calculated LPS values.
- Space Complexity: .
Edge Cases
- Single Character String (
s = "a"):- Loop condition
suffix < n() evaluates to false immediately. Returns[0].
- Loop condition
- All Identical Characters (
s = "aaaa"):- Matches continuously. Output:
[0, 1, 2, 3].
- Matches continuously. Output:
- No Repeats (
s = "abcdef"):- Constantly fails match condition, hits
prefix == 0subcase. Output:[0, 0, 0, 0, 0, 0].
- Constantly fails match condition, hits
10. Summary and Key Takeaways
- Definition of LPS: Represents the length of the longest proper prefix of substring
s[0...i]that is also a suffix ofs[0...i]. - Proper Constraint: The entire string itself can never be considered its own prefix or suffix ().
- Zero Backtracking: The
suffixpointer moves strictly left-to-right from index1to and never decreases. - The Fallback Mechanism: On a character mismatch when
prefix > 0, the pointer falls back usingprefix = LPS[prefix - 1]. - Dual Role of
LPS[i]: Stores both the length of the matching pattern and the index location for the next prefix character comparison. - Amortized Efficiency: Decreases to
prefixare bounded by prior increments, ensuring a linear execution time of . - Foundation for KMP: Constructing the LPS array in time is the prerequisite step for executing full pattern matching in time.