Definition
We first introduce the concept of Lyndon factorization.
For a string , if the lexicographic order of is strictly smaller than that of every suffix of , we call a simple string, or a Lyndon string. For example, a, b, ab, aab, abb, ababb, and abcd are all Lyndon strings. Equivalently, is a Lyndon string if and only if its lexicographic order is strictly smaller than that of every nontrivial conjugate string of (nontrivial means nonempty and different from itself).
The Lyndon factorization of a string is written as , where every is a Lyndon string and their lexicographic orders are sorted in non-increasing order, that is, . This factorization exists and is unique.
Duval algorithm
Explanation
Duval's algorithm finds the Lyndon factorization of a string in time.
First, we introduce another concept. If a string can be decomposed in the form , where is a Lyndon string and is a prefix of ( may be the empty string), then is called a pre-simple string, or a pre-Lyndon string. Every Lyndon string is also a pre-Lyndon string.
Duval's algorithm is greedy. During the algorithm, we split into three parts, , where is a Lyndon string whose Lyndon factorization has already been recorded; is a pre-Lyndon string; and is the unprocessed part.
Procedure
In broad terms, at each step the algorithm tries to append the first character of to the end of . If is no longer a pre-Lyndon string, we can cut off a prefix of (that is, a Lyndon factor) and append it to the end of .
Let pointer point to the first character of , so that ranges from to , the length of the string. During the loop, let pointer point to the first character of , and pointer point to the character currently being considered in (that is, the character corresponding to in the preceding period of ). Our goal is to append to the end of , so we compare with :
- If , appending to the end of does not affect its pre-simple property. We only need to increment pointers , moving them to the next positions.
- If , then becomes a Lyndon string. We increment and let point to the first character of , so that becomes a new Lyndon string with one repetition.
- If , then is no longer pre-simple. We must split a Lyndon substring from ; its length is , namely the length of one period. We then replace with the part remaining after the split and continue the loop (note that in this case we do not change pointers ), until the period has been completely removed. For the remaining part, we only need to roll the progress back to the beginning of that part.
Implementation
The following code returns the Lyndon factorization of the string .
C++
// duval_algorithm
vector<string> duval(string const& s) {
int n = s.size(), i = 0;
vector<string> factorization;
while (i < n) {
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k) {
factorization.push_back(s.substr(i, j - k));
i += j - k;
}
}
return factorization;
}
Python
# duval_algorithm
def duval(s):
n, i = len(s), 0
factorization = []
while i < n:
j, k = i + 1, i
while j < n and s[k] <= s[j]:
if s[k] < s[j]:
k = i
else:
k += 1
j += 1
while i <= k:
factorization.append(s[i : i + j - k])
i += j - k
return factorization
Complexity analysis
Next, we prove the complexity of this algorithm.
The outer loop runs at most times, because increases on every iteration. The second inner loop is also , since it only records the Lyndon factorization. We now analyze the inner loop. It is easy to see that every Lyndon string found in an iteration of the outer loop is longer than the remaining string that we have compared. Therefore, the sum of the lengths of the remaining strings is less than , and the inner loop runs at most times. In fact, the total number of iterations is at most , so the time complexity is .
Minimal representation (Finding the smallest cyclic shift)
For a string of length , denoted by , we can use the algorithm above to find its minimal representation.
We construct the Lyndon factorization of , then find a Lyndon string in this factorization whose starting position is less than and whose ending position is at least . It is easy to prove from the properties of Lyndon factorization that the first character of is the first character of the minimal representation of ; in other words, starting from the beginning of , the string of the next characters is the minimal representation of .
Thus, during the factorization, we only need to record the beginning of each pre-Lyndon string.
C++
// smallest_cyclic_string
string min_cyclic_string(string s) {
s += s;
int n = s.size();
int i = 0, ans = 0;
while (i < n / 2) {
ans = i;
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k) i += j - k;
}
return s.substr(ans, n / 2);
}
Python
# smallest_cyclic_string
def min_cyclic_string(s):
s += s
n = len(s)
i, ans = 0, 0
while i < n / 2:
ans = i
j, k = i + 1, i
while j < n and s[k] <= s[j]:
if s[k] < s[j]:
k = i
else:
k += 1
j += 1
while i <= k:
i += j - k
return s[ans : ans + n / 2]
Exercises
-
This page is mainly translated from the blog post Lyndon Decomposition. Duval's Algorithm. Finding the Smallest Cyclic Shift and its English translation Lyndon factorization. The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.