Lyndon factorization

Definition

We first introduce the concept of Lyndon factorization.

For a string ss, if the lexicographic order of ss is strictly smaller than that of every suffix of ss, we call ss a simple string, or a Lyndon string. For example, a, b, ab, aab, abb, ababb, and abcd are all Lyndon strings. Equivalently, ss is a Lyndon string if and only if its lexicographic order is strictly smaller than that of every nontrivial conjugate string of ss (nontrivial means nonempty and different from itself).

The Lyndon factorization of a string ss is written as s=w1w2⋯wks=w_1w_2\cdots w_k, where every wiw_i is a Lyndon string and their lexicographic orders are sorted in non-increasing order, that is, w1≥w2≥⋯≥wkw_1\ge w_2\ge\cdots\ge w_k. This factorization exists and is unique.

Duval algorithm

Explanation

Duval's algorithm finds the Lyndon factorization of a string in O(n)O(n) time.

First, we introduce another concept. If a string tt can be decomposed in the form t=ww⋯w‾t=ww\cdots\overline{w}, where ww is a Lyndon string and w‾\overline{w} is a prefix of ww (w‾\overline{w} may be the empty string), then tt 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 ss into three parts, s=s1s2s3s=s_1s_2s_3, where s1s_1 is a Lyndon string whose Lyndon factorization has already been recorded; s2s_2 is a pre-Lyndon string; and s3s_3 is the unprocessed part.

Procedure

In broad terms, at each step the algorithm tries to append the first character of s3s_3 to the end of s2s_2. If s2s_2 is no longer a pre-Lyndon string, we can cut off a prefix of s2s_2 (that is, a Lyndon factor) and append it to the end of s1s_1.

Let pointer ii point to the first character of s2s_2, so that ii ranges from 11 to nn, the length of the string. During the loop, let pointer jj point to the first character of s3s_3, and pointer kk point to the character currently being considered in s2s_2 (that is, the character corresponding to jj in the preceding period of s2s_2). Our goal is to append s[j]s[j] to the end of s2s_2, so we compare s[j]s[j] with s[k]s[k]:

  1. If s[j]=s[k]s[j]=s[k], appending s[j]s[j] to the end of s2s_2 does not affect its pre-simple property. We only need to increment pointers j,kj,k, moving them to the next positions.
  2. If s[j]>s[k]s[j]>s[k], then s2s[j]s_2s[j] becomes a Lyndon string. We increment jj and let kk point to the first character of s2s_2, so that s2s_2 becomes a new Lyndon string with one repetition.
  3. If s[j]<s[k]s[j]<s[k], then s2s[j]s_2s[j] is no longer pre-simple. We must split a Lyndon substring from s2s_2; its length is j−kj-k, namely the length of one period. We then replace s2s_2 with the part remaining after the split and continue the loop (note that in this case we do not change pointers j,kj,k), 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 ss.

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 nn times, because ii increases on every iteration. The second inner loop is also O(n)O(n), 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 nn, and the inner loop runs at most O(n)O(n) times. In fact, the total number of iterations is at most 4n−34n-3, so the time complexity is O(n)O(n).

Minimal representation (Finding the smallest cyclic shift)

For a string of length nn, denoted by ss, we can use the algorithm above to find its minimal representation.

We construct the Lyndon factorization of ssss, then find a Lyndon string tt in this factorization whose starting position is less than nn and whose ending position is at least nn. It is easy to prove from the properties of Lyndon factorization that the first character of tt is the first character of the minimal representation of ss; in other words, starting from the beginning of tt, the string of the next nn characters is the minimal representation of ss.

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 content comes from OI Wiki: Lyndon factorization. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.