Definitions of String Prefixes and Suffixes
For definitions of prefixes, proper prefixes, suffixes, and proper suffixes of strings, see String Basics.
Prefix Function
Definition
Given a string of length , denoted , its prefix function is defined as an array of length denoted . The definition of is:
- If the substring has a pair of equal proper prefix and proper suffix, namely and , then is the length of this equal proper prefix (or proper suffix, since they are equal), that is, ;
- If there is more than one such pair, is the length of the longest pair;
- If no equal pair exists, .
In simple terms, is the length of the longest equal proper prefix and proper suffix of the substring .
In mathematical notation:
In particular, define .
Process
For example, consider the string abcabcd.
, because a has no proper prefix or proper suffix, so it is 0 by definition.
, because ab has no equal proper prefix and proper suffix.
, because abc has no equal proper prefix and proper suffix.
, because abca has exactly one pair of equal proper prefix and proper suffix: a, of length 1.
, because the only equal proper prefix and proper suffix of abcab is ab, of length 2.
, because the only equal proper prefix and proper suffix of abcabc is abc, of length 3.
, because abcabcd has no equal proper prefix and proper suffix.
Similarly, the prefix function of aabaaab is .
Naive Algorithm for Computing the Prefix Function
Process
An algorithm that computes the prefix function directly from its definition proceeds as follows:
- In a loop, for , compute the prefix-function values in order, with assigned .
- To compute the current prefix-function value , try the variable from the largest possible proper-prefix length downward.
- If the proper prefix and proper suffix of the current length are equal, then this length is ; otherwise, decrease j by 1 and continue matching until .
- If and no match has been found, set and move to the next index .
Implementation
The implementation is as follows:
C++
// Note:
// string substr (size_t pos = 0, size_t len = npos) const;
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n);
for (int i = 1; i < n; i++)
for (int j = i; j >= 0; j--)
if (s.substr(0, j) == s.substr(i - j + 1, j)) {
pi[i] = j;
break;
}
return pi;
}
Python
def prefix_function(s):
n = len(s)
pi = [0] * n
for i in range(1, n):
for j in range(i, -1, -1):
if s[0:j] == s[i - j + 1 : i + 1]:
pi[i] = j
break
return pi
Java
static int[] prefix_function(String s) {
int n = s.length();
int[] pi = new int[n];
for (int i = 1; i < n; i++) {
for (int j = i; j >= 0; j--) {
if (s.substring(0, j).equals(s.substring(i - j + 1, i + 1))) {
pi[i] = j;
break;
}
}
}
return pi;
}
The time complexity of this algorithm is , leaving considerable room for improvement.
Efficient Algorithms for Computing the Prefix Function
First Optimization
The first important observation is that adjacent prefix-function values increase by at most .
As shown below, when taking the largest possible , the newly added character must match the corresponding character, namely ; in this case, .
Therefore, when moving to the next position, the prefix-function value either increases by one, remains unchanged, or decreases.
Implementation
The improved algorithm at this point is:
C++
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n);
for (int i = 1; i < n; i++)
for (int j = pi[i - 1] + 1; j >= 0; j--) // improved: j=i => j=pi[i-1]+1
if (s.substr(0, j) == s.substr(i - j + 1, j)) {
pi[i] = j;
break;
}
return pi;
}
Python
def prefix_function(s):
n = len(s)
pi = [0] * n
for i in range(1, n):
for j in range(pi[i - 1] + 1, -1, -1):
if s[0:j] == s[i - j + 1 : i + 1]:
pi[i] = j
break
return pi
Java
static int[] prefix_function(String s) {
int n = s.length();
int[] pi = new int[n];
for (int i = 1; i < n; i++) {
for (int j = pi[i - 1] + 1; j >= 0; j--) {
if (s.substring(0, j).equals(s.substring(i - j + 1, i + 1))) {
pi[i] = j;
break;
}
}
}
return pi;
}
In this first improved algorithm, when computing each , the best case is that the first string comparison produces a match, so the baseline number of string comparisons is .
Because j = pi[i-1]+1 (pi[0]=0) limits the maximum number of string comparisons, we can see that only the best case adds to the upper bound on the number of comparisons; every comparison beyond the first consumes room for growth in later iterations.
Thus the case with the maximum number of comparisons has at least comparison consumed and at most comparisons accumulated, giving comparisons.
Therefore, after this optimization, computing the prefix function requires only string comparisons, and the total complexity decreases to .
Second Optimization
The first optimization considered the best case when computing : , giving . Let us go further and consider how to jump when .
As shown above, when a mismatch occurs, we want to find, for the substring , the second-largest length after , namely , while preserving the prefix property at position , namely :
If such a length is found, only and need to be compared again. If they are equal, then . Otherwise, for the substring , find the second-largest length after , namely , while preserving the prefix property, and repeat until . If , then . The second comparison is illustrated below.
From the figure, because , for the second-largest length is , with the property
The diagram for this formula is shown below:
In other words, is equivalent to the prefix-function value of the substring , corresponding to the lower half of the figure above, namely . Similarly, the second-largest length after is equivalent to the prefix-function value of , .
Clearly, we obtain the following state transition for : .
Final Algorithm
Thus we can construct an algorithm that performs no string comparisons and only operations.
Moreover, its implementation is surprisingly short and intuitive:
Implementation
C++
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n);
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;
}
Python
def prefix_function(s):
n = len(s)
pi = [0] * n
for i in range(1, n):
j = pi[i - 1]
while j > 0 and s[i] != s[j]:
j = pi[j - 1]
if s[i] == s[j]:
j += 1
pi[i] = j
return pi
Java
static int[] prefix_function(String s) {
int n = s.length();
int[] pi = new int[n];
for (int i = 1; i < n; i++) {
int j = pi[i - 1];
while (j > 0 && s.charAt(i) != s.charAt(j)) {
j = pi[j - 1];
}
if (s.charAt(i) == s.charAt(j)) {
j++;
}
pi[i] = j;
}
return pi;
}
This is an online algorithm: it processes data as it arrives. For example, the string can be read one character at a time and the prefix-function value of each character can be computed immediately. The algorithm still needs to store the string itself and the previously computed prefix-function values, but if the maximum possible prefix-function value of the string is known in advance to be , then it is enough to store the first characters and their corresponding prefix-function values.
Applications
Finding a Substring in a String: The Knuth–Morris–Pratt Algorithm
This algorithm was jointly published by Knuth, Pratt, and Morris in 19771. This task is a typical application of the prefix function.
Process
Given a text and a string , find and report all occurrences of in .
For convenience, let denote the length of , and the length of the text .
Construct the string , where is a delimiter that appears in neither nor , and compute its prefix function. Now consider the meaning of the remaining prefix-function values after the first values, which belong to and the delimiter. By definition, is the length of the longest proper substring whose right endpoint is and which is also a prefix; in this setting, it is the length of the longest substring matching a prefix of and ending at . Because the delimiter exists, this length cannot exceed . If , then occurs in full at this position, that is, its right endpoint is at position . Note that this index is with respect to the string .
Therefore, at a position , if , then the string occurs in at position . The diagram below illustrates the indices.
As mentioned when computing the prefix function, if we know that the prefix-function value never exceeds a fixed value, we do not need to store the entire string and prefix function; only their initial parts are needed. In this case, we only need to store and the corresponding prefix-function values. We can read one character of at a time and compute the prefix-function value at the current position.
Thus the Knuth–Morris–Pratt algorithm (KMP) solves the problem in time and memory.
Implementation
C++
vector<int> find_occurrences(string text, string pattern) {
string cur = pattern + '#' + text;
int sz1 = text.size(), sz2 = pattern.size();
vector<int> v;
vector<int> lps = prefix_function(cur);
for (int i = sz2 + 1; i <= sz1 + sz2; i++) {
if (lps[i] == sz2) v.push_back(i - 2 * sz2);
}
return v;
}
Python
def find_occurrences(t, s):
cur = s + "#" + t
sz1, sz2 = len(t), len(s)
ret = []
lps = prefix_function(cur)
for i in range(sz2 + 1, sz1 + sz2 + 1):
if lps[i] == sz2:
ret.append(i - 2 * sz2)
return ret
Java
static List<Integer> find_occurrences(String text, String pattern) {
String cur = pattern + '#' + text;
int sz1 = text.length(), sz2 = pattern.length();
List<Integer> v = new ArrayList<>();
int[] lps = prefix_function(cur);
for (int i = sz2 + 1; i <= sz1 + sz2; i++) {
if (lps[i] == sz2) {
v.add(i - 2 * sz2);
}
}
return v;
}
Periods of a String
For a string and , if holds for every , then is called a period of .
For a string and , if the prefix of of length equals the suffix of length , then the prefix of of length is called a border of .
The existence in of a border of length implies that is a period of .
By the definition of the prefix function, all border lengths of are .2
Therefore, in time, all periods of can be found. Since is the length of the longest border of , is the shortest period of .
Counting the Occurrences of Each Prefix
We discuss two problems. Given a string of length , denoted , the first variant asks for the number of occurrences of each prefix in the same string, while the second asks for the number of occurrences of each prefix in another given string .
First consider the first problem. At position , consider the prefix-function value . By definition, it means that a prefix of of length occurs at position and ends at , and that no longer prefix satisfies this definition. At the same time, shorter prefixes may also end at this position. We encounter a question already answered when computing the prefix function: given a prefix of length that is also a suffix ending at , what is the next smaller prefix length that is also a suffix ending at ? Therefore, with right endpoint , there is a prefix of length , a prefix of length , a prefix of length , and so on, until the length becomes . Thus we can compute the answer as follows.
Implementation
C++
vector<int> ans(n + 1);
for (int i = 0; i < n; i++) ans[pi[i]]++;
for (int i = n - 1; i > 0; i--) ans[pi[i - 1]] += ans[i];
for (int i = 0; i <= n; i++) ans[i]++;
Python
ans = [0] * (n + 1)
for i in range(0, n):
ans[pi[i]] += 1
for i in range(n - 1, 0, -1):
ans[pi[i - 1]] += ans[i]
for i in range(0, n + 1):
ans[i] += 1
Explanation
In the code above, first count how many times each prefix-function value appears in the array , then compute the final answer: if a prefix of length occurs exactly times, this value must be added to the occurrence count of its longest substring that is both a suffix and a prefix. Finally, to count the original prefix, add to each result.
Now consider the second problem. Apply the technique from Knuth–Morris–Pratt: construct and compute its prefix function. The only difference from the first problem is that we care only about prefix-function values related to , namely those with , namely . Once these values are known, apply the same algorithm as in the first problem.
Number of Distinct Substrings in a String
Given a string of length , denoted , we want to compute the number of distinct substrings.
We solve this problem iteratively. In other words, given the current number of distinct substrings, we need a way to recompute it after appending one character to the end of .
Let be the current number of distinct substrings of . Append a new character to . Clearly, some new substrings ending in will appear. We want to count those ending in this character that have not appeared before.
Construct and reverse it to obtain . The task is now to determine how many prefixes of do not occur anywhere else in . If the maximum prefix-function value of is , then the longest prefix that occurs in has length . Naturally, every shorter prefix also occurs.
Therefore, the number of new substrings after adding a new character is .
Thus, for each appended character, the number of new substrings can be computed in time, giving total complexity .
It is worth noting that we can also recompute the number of distinct substrings when adding a character at the beginning, or removing a character from the end or beginning.
String Compression
Given a string of length , denoted , find its shortest “compressed” representation: the shortest string such that can be represented by concatenating one or more copies of .
Clearly, we only need to find the length of . Once the length is known, the answer is the prefix of of that length.
Compute the prefix function of . Using its final value , define . We will prove that if divides , then is the answer; otherwise no valid compression exists, and the answer is .
Assume that is divisible by . Then the string can be divided into blocks of length . By the definition of the prefix function, its prefix of length equals its suffix. This means that the last block equals the second-to-last block, the second-to-last equals the third-to-last, and so on. Consequently, all blocks are equal, so can be compressed to length .
Proof
We still need to prove that this value is optimal. Indeed, if there were a compressed representation shorter than , then the final prefix-function value would necessarily be greater than . Thus is the answer.
Now suppose that is not divisible by . We prove by contradiction that the answer is 3. Assume that the shortest compressed representation has length ( divides ), so is divided into blocks. Then the final prefix-function value must be greater than (otherwise would be divisible by ), so the suffix it represents partially covers the first block. Now consider the second block of the string. It has two descriptions: the first is , and the second is . Since both descriptions represent the same string, we obtain a system of equations, which can be written as , where denotes the least nonnegative remainder modulo .
By the extended Euclidean algorithm, we can obtain and satisfying . By appropriately adding multiples of the equation , we can obtain and satisfying . This means that by repeatedly applying equations from the system above, we obtain the new system .
Since divides , this means that is a period of . Moreover, because , we have , so is a period smaller than of . Therefore, has a compressed representation of length , contradicting the minimality of .
In conclusion, no compressed representation of length less than exists, so the answer is .
Constructing an Automaton from the Prefix Function
Let us return to the new string formed by concatenating two strings with a delimiter. For strings and , compute the prefix function of . Clearly, because is a delimiter, the prefix-function value never exceeds . Therefore, we only need to store and its corresponding prefix-function values, after which we can compute the prefix-function values for all subsequent characters dynamically:
In fact, knowing the next character of string , namely , and the prefix-function value at the previous position is enough to compute the next prefix-function value; no other character or prefix-function value of is needed.
In other words, we can construct an automaton (a finite-state machine): its state is the current prefix-function value, and the transition from one state to another is determined by the next character.
Thus, even without the string , we can construct a transition table using the construction algorithm:
Implementation
void compute_automaton(string s, vector<vector<int>>& aut) {
s += '#';
int n = s.size();
vector<int> pi = prefix_function(s);
aut.assign(n, vector<int>(26));
for (int i = 0; i < n; i++) {
for (int c = 0; c < 26; c++) {
int j = i;
while (j > 0 && 'a' + c != s[j]) j = pi[j - 1];
if ('a' + c == s[j]) j++;
aut[i][c] = j;
}
}
}
In this form, for a lowercase alphabet, the time complexity is . We can apply dynamic programming to reuse the parts already computed in the table. Whenever we move from to , we are saying that the state reached by transition is the same as that reached by transition , and the latter answer has already been computed exactly.
Implementation
void compute_automaton(string s, vector<vector<int>>& aut) {
s += '#';
int n = s.size();
vector<int> pi = prefix_function(s);
aut.assign(n, vector<int>(26));
for (int i = 0; i < n; i++) {
for (int c = 0; c < 26; c++) {
if (i > 0 && 'a' + c != s[i])
aut[i][c] = aut[pi[i - 1]][c];
else
aut[i][c] = i + ('a' + c == s[i]);
}
}
}
The automaton can ultimately be constructed in time.
When is this automaton useful? First, recall that most of the time we use the prefix function of for one purpose: finding all occurrences of in .
Therefore, the most direct benefit of this automaton is accelerating the computation of the prefix function of .
By constructing the automaton for , we no longer need to store or its corresponding prefix-function values. All transitions have already been computed in the table.
There is also a second, less direct application. When is a huge string constructed by rules, the automaton can accelerate the computation. Gray strings and strings recursively constructed from short input strings are examples.
For completeness, consider the following problem: given a number and a string of length , denoted , compute the number of occurrences of in the -th Gray string. Recall that Gray strings are defined as follows:
Because of its astronomical length, even constructing is impossible here: the -th Gray string has characters. However, its final prefix-function value can be computed efficiently if only a few prefix-function values at the beginning are known.
In addition to the automaton, compute , the automaton state after starting from state and processing , and , after starting from state and processing , the number of occurrences of in . In fact, is the number of times the prefix function takes the value during the operation. The answer is .
How should these values be computed? By definition, the initial conditions are and . All subsequent values can be obtained from earlier values and the automaton. To compute the values for a particular , recall that is the concatenation of , the -th character of the alphabet, and . Thus the automaton passes through the following states:
The value of can be computed similarly.
Here, is when its expression is true and otherwise. In summary, this solves the Gray-string problem and a large class of similar problems. For example, the same method solves the following problem: given a string and a collection of patterns , each pattern consists of ordinary characters and may recursively insert previously defined strings in the form , meaning that must be inserted times at that position. Here is an example of such patterns:
Recursive substitution makes the string lengths grow explosively; they can even reach an order of magnitude of . We need to find the number of occurrences of string in each string.
This problem can likewise be solved by constructing a prefix-function automaton. As before, use previously computed results to calculate the transitions for each pattern and then count the answers.
Exercises
- UVa 455 "Periodic Strings"
- UVa 11022 "String Factoring"
- UVa 11452 "Dancing the Cheeky-Cheeky"
- UVa 12604 - Caesar Cipher
- UVa 12467 - Secret Word
- UVa 11019 - Matrix Matcher
- SPOJ - Pattern Find
- Codeforces - Anthem of Berland
- Codeforces - MUH and Cube Walls
References and Notes
This page was mainly translated from the article Префикс-функция. Алгоритм Кнута-Морриса-Пратта and its English translation Prefix function. Knuth–Morris–Pratt algorithm. The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.
Footnotes
-
Knuth, Donald E., James H. Morris, Jr, and Vaughan R. Pratt. "Fast pattern matching in strings." SIAM journal on computing 6.2 (1977): 323-350.doi: 10.1137/0206024 ↩
-
The proof of this part in both the Russian and English versions appears to be incorrect. This part of the proof was added by the author of this article. ↩