Main–Lorentz algorithm

Tandem repetitions

Definition

Given a string ss of length nn.

The new string produced by writing a string twice consecutively is called a tandem repetition. Below, for precision, we call the string being repeated the original string. In other words, a tandem repetition is equivalent to a pair of indices (i,j)(i, j) such that s[i…j]s[i \dots j] is the concatenation of two identical strings.

Your goal is to find all tandem repetitions in a given string ss. Alternatively, solve the simpler problem of finding any tandem repetition or the longest tandem repetition in ss.

The algorithm below was proposed by Michael Main and Richard J. Lorentz in 1982.

Conventions

All string indices below start from 00.

Below, let s‾\overline{s} denote the reverse of ss. For example, abc‾=cba\overline{\tt abc} = \tt cba.

Explanation

Consider the string acababaee\tt acababaee. It contains three tandem repetitions:

  • s[2…5]=ababs[2 \dots 5] = \tt abab
  • s[3…6]=babas[3 \dots 6] = \tt baba
  • s[7…8]=ees[7 \dots 8] = \tt ee

As another example, consider the string abaaba\tt abaaba. It contains only two tandem repetitions:

  • s[0…5]=abaabas[0 \dots 5] = \tt abaaba
  • s[2…3]=aas[2 \dots 3] = \tt aa

Number of tandem repetitions

A string of length nn may have as many as O(n2)O(n^2) tandem repetitions. An obvious example is a string consisting of nn identical letters: in this case, any substring of even length is a tandem repetition. In most cases, a periodic string with a relatively short period has many tandem repetitions.

However, this does not prevent us from calculating the number of tandem repetitions in O(nlog⁡n)O(n \log n) time. This is because the algorithm expresses a tandem repetition in a compressed form, allowing multiple tandem repetitions to be compressed into one.

Here are some interesting conclusions about the number of tandem repetitions:

  • If the original string of a tandem repetition is not itself a tandem repetition, we call this tandem repetition a primitive repetition. It can be proved that there are at most O(nlog⁡n)O(n \log n) primitive repetitions.
  • If we compress a tandem repetition using a Crochemore triple (i,p,r)(i, p, r), where ii is the starting position of the tandem repetition, pp is the length of one period of the tandem repetition (note that it is not the length of the original string), and rr is the number of repetitions of this period, then all tandem repetitions of a string can be represented by O(nlog⁡n)O(n \log n) Crochemore triples.
  • Fibonacci strings are defined as follows:
t0=a,t1=b,ti=ti−1+ti−2,\begin{align} t_0 &= a, \\ t_1 &= b, \\ t_i &= t_{i-1} + t_{i-2}, \end{align}

It can be seen that Fibonacci strings are highly periodic. For a Fibonacci string tit_i of length fif_i, even when compressed using Crochemore triples, there are O(filog⁡fi)O(f_i \log f_i) triples. The number of its primitive repetitions is also O(filog⁡fi)O(f_i \log f_i).

Main–Lorentz algorithm

Explanation

The core idea of the Main–Lorentz algorithm is divide and conquer.

The algorithm divides the string into left and right parts. It first calculates the number of tandem repetitions completely within the left (or right) part, and then calculates the number of tandem repetitions whose starting position is in the left part and whose ending position is in the right part. (Below, we call such tandem repetitions crossing repetitions.)

Calculating the number of crossing repetitions is the key point of the Main–Lorentz algorithm, and we will examine it in detail below.

Process

Finding crossing repetitions

Let the left and right parts of a string be u,vu, v. Then s=u+vs = u + v, and the lengths of uu and vv are approximately half the length of ss.

For any tandem repetition, consider its middle character. Here, we call the first character in the right half of a tandem repetition its middle character; in other words, if s[i...j]s[i...j] is a tandem repetition, its middle character is s[(i+j+1)/2]s[(i + j + 1)/2]. If the middle character of a tandem repetition is in uu, we call it left; otherwise, we call it right.

Next, we will discuss how to find all left repetitions.

Let the length of a left repetition be 2l2l. Consider the first character of this repetition that falls in vv (namely s[∣u∣]s[|u|]). This character must match some character u[cntr]u[\textit{cntr}] in uu.

We fix cntr\textit{cntr} and find all repetitions satisfying the conditions. For example, for the string c  acntr  c  ∣  a  d  a\tt c \; \underset{\textit{cntr}}{a} \; c \; | \; a \; d \; a (where ∣\tt | is used to distinguish the left and right parts), fixing cntr=1cntr = 1, we can see that the tandem repetition caca\tt caca satisfies the requirements.

Clearly, once cntr\textit{cntr} is fixed, the value of ll is fixed as well. Once we know how to find all repetitions, we can enumerate cntr\textit{cntr} from 00 to ∣u∣−1|u| - 1 and find all repetitions satisfying the conditions.

Determining left repetitions

Even after fixing cntr\textit{cntr}, there may still be multiple tandem repetitions satisfying the conditions. How do we find all of them?

Consider another example. In the string abcabcac\tt abcabcac, take the tandem repetition a⏞l1bcntrc⏞l2a⏞l1  ∣  b  c⏞l2\overbrace{\tt a}^{l_1} \overbrace{\underset{\textit{cntr}}{\tt b} \tt c}^{l_2} \overbrace{\tt a}^{l_1} \; | \; \overbrace{\tt b \; \tt c}^{l_2}. Let l1l_1 be the length of the substring from the first character of this tandem repetition to s[cntr−1]s[\textit{cntr} - 1], and let l2l_2 be the length of the substring from s[cntr]s[\textit{cntr}] to the last character of the original string on the left side of the tandem repetition.

Thus, we can give a necessary and sufficient condition for a substring of length 2l=2(l1+l2)=2(∣u∣−cntr)2l = 2(l_1 + l_2) = 2(|u| - \textit{cntr}) to be a tandem repetition:

Let k1k_1 be the largest integer such that u[cntr−k1…cntr−1]=u[∣u∣−k1…∣u∣−1]u[\textit{cntr} - k_1 \dots \textit{cntr} - 1] = u[|u| - k_1 \dots |u| - 1], and let k2k_2 be the largest integer such that u[cntr…cntr+k2−1]=v[0…k2−1]u[\textit{cntr} \dots \textit{cntr} + k_2 - 1] = v[0 \dots k_2 - 1]. Then for every pair (l1,l2)(l_1, l_2) satisfying l1≤k1l_1 \leq k_1 and l2≤k2l_2 \leq k_2, we can find exactly one corresponding tandem repetition.

In summary:

  • Fix a cntr\textit{cntr}.
  • Then all tandem repetitions we seek have length 2l=2(∣u∣−cntr)2l = 2(|u| - \textit{cntr}). There may still be multiple repetitions satisfying the conditions, depending on the values of l1l_1 and l2l_2.
  • Calculate the k1k_1 and k2k_2 mentioned above.
  • Then all tandem repetitions satisfying the conditions satisfy:
l1+l2=l=∣u∣−cntrl1≤k1,l2≤k2.\begin{align} l_1 + l_2 &= l = |u| - \textit{cntr} \\ l_1 &\le k_1, \\ l_2 &\le k_2. \\ \end{align}

Next, we only need to consider how to calculate k1k_1 and k2k_2 quickly. With the help of the Z-function, we can calculate them in O(1)O(1):

  • To calculate k1k_1: calculate the Z-function of u‾\overline{u}.
  • To calculate k2k_2: calculate the Z-function of v+#+uv + \# + u, where #\# is a character that appears in neither uu nor vv.

Right repetitions

The method for calculating right repetitions is almost identical to that for calculating left repetitions. Consider the first character of this repetition that falls in uu (namely s[∣u∣−1]s[|u| - 1]). It must match some character in vv; let the position of this character in vv be cntr\textit{cntr}.

Let k1k_1 be the largest integer such that v[cntr−k1+1…cntr]=u[∣u∣−k1…∣u∣−1]v[\textit{cntr} - k_1 + 1 \dots \textit{cntr}] = u[|u| - k_1 \dots |u| - 1], and k2k_2 be the largest integer such that v[cntr+1…cntr+k2]=v[0…k2−1]v[\textit{cntr} + 1 \dots \textit{cntr} + k_2] = v[0 \dots k_2 - 1]. Then we can obtain k1k_1 and k2k_2 by calculating the Z-functions of u‾+#+v‾\overline{u} + \# + \overline{v} and vv, respectively.

Enumerate cntr\textit{cntr} and find right repetitions in a similar manner.

Implementation

The Main–Lorentz algorithm gives all tandem repetitions in the form of quadruples (cntr,l,k1,k2)(\textit{cntr}, l, k_1, k_2). If you only need to calculate the number of tandem repetitions or find the longest one, the information in this quadruple is sufficient. By the master theorem, the time complexity of the Main–Lorentz algorithm is O(nlog⁡n)O(n \log n).

Note that if you want to use these quadruples to find the starting and ending positions of all tandem repetitions, the worst-case time complexity reaches O(n2)O(n^2). We implement this below, storing the starting and ending positions of all tandem repetitions in repetitions.

vector<int> z_function(string const& s) {
  int n = s.size();
  vector<int> z(n);
  for (int i = 1, l = 0, r = 0; i < n; i++) {
    if (i <= r) z[i] = min(r - i + 1, z[i - l]);
    while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
    if (i + z[i] - 1 > r) {
      l = i;
      r = i + z[i] - 1;
    }
  }
  return z;
}

int get_z(vector<int> const& z, int i) {
  if (0 <= i && i < (int)z.size())
    return z[i];
  else
    return 0;
}

vector<pair<int, int>> repetitions;

void convert_to_repetitions(int shift, bool left, int cntr, int l, int k1,
                            int k2) {
  for (int l1 = max(1, l - k2); l1 <= min(l, k1); l1++) {
    if (left && l1 == l) break;
    int l2 = l - l1;
    int pos = shift + (left ? cntr - l1 : cntr - l - l1 + 1);
    repetitions.emplace_back(pos, pos + 2 * l - 1);
  }
}

void find_repetitions(string s, int shift = 0) {
  int n = s.size();
  if (n == 1) return;

  int nu = n / 2;
  int nv = n - nu;
  string u = s.substr(0, nu);
  string v = s.substr(nu);
  string ru(u.rbegin(), u.rend());
  string rv(v.rbegin(), v.rend());

  find_repetitions(u, shift);
  find_repetitions(v, shift + nu);

  vector<int> z1 = z_function(ru);
  vector<int> z2 = z_function(v + '#' + u);
  vector<int> z3 = z_function(ru + '#' + rv);
  vector<int> z4 = z_function(v);

  for (int cntr = 0; cntr < n; cntr++) {
    int l, k1, k2;
    if (cntr < nu) {
      l = nu - cntr;
      k1 = get_z(z1, nu - cntr);
      k2 = get_z(z2, nv + 1 + cntr);
    } else {
      l = cntr - nu + 1;
      k1 = get_z(z3, nu + 1 + nv - 1 - (cntr - nu));
      k2 = get_z(z4, (cntr - nu) + 1);
    }
    if (k1 + k2 >= l) convert_to_repetitions(shift, cntr < nu, cntr, l, k1, k2);
  }
}

This page is mainly adapted from the blog Поиск всех тандемных повторов в строке. Алгоритм Мейна-Лоренца and its English translation Finding repetitions. The Russian version is licensed under Public Domain + Leave a Link; the English version is licensed under CC-BY-SA 4.0.

This content comes from OI Wiki: Main–Lorentz algorithm. 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.