E109. Поиск всех тандемных повторов в строке. Thuật toán Мейна-Лоренца

e-maxx algorithm original: C/C++ #algorithm #emaxx #search #string
Văn bản bài toán được dịch từ tiếng Nga theo ngôn ngữ giao diện. Mã không thay đổi.

Источник: e-maxx.ru/algo, страница PDF 343.

Дана chuỗi

длины

. Тандемным повтором (tandem repeat) в ней называются два вхождения какой-либо подстроки подряд.

Иными словами, тандемный повтор описывается парой индексов

такими, что substring

— это

две одинаковые строки, записанные подряд. Bài toán заключается в том, чтобы find все тандемные повторы. Упрощённые варианты этой задачи: find любой тандемный повтор или find длиннейший тандемный повтор. Примечание. Во избежание путаницы все строки в статье мы будем считать 0-индексированными, т.е. первый символ строки имеет индекс 0. Описываемый здесь Thuật toán был опубликован в 1982 г. Мейном и Лоренцем (см. список литературы).

Ví dụ

Рассмотрим тандемные повторы на Ví dụе какой-нибудь простой строки, наVí dụ: В этой строке присутствуют следующие тандемные повторы:

Другой Ví dụ: Здесь есть только два тандемных повтора:

number тандемных повторов

Вообще говоря, тандемных повторов в строке длины

может быть порядка

.

Очевидным Ví dụом является chuỗi, составленная из

одинаковых букв — в такой строке тандемным

повтором является любая substring чётной длины, которых Ví dụно

. Вообще, любая периодичная chuỗi

с коротким периодом будет содержать очень много тандемных повторов С другой стороны, сам по себе этот факт никак не препятствует существованию Thuật toánа с асимптотикой , поскольку Thuật toán может выдавать тандемные повторы в том или ином сжатом виде, группами по несколько штук сразу. Более того, существует понятие серий — четвёрок чисел, которые описывают целую группу периодических подстрок. Было доказано, что number серий в любой строке линейно по отношению к длине строки. Впрочем, описываемый ниже Thuật toán не использует понятие серий, поэтому не будем детализировать это понятие. Приведём здесь и другие интересные результаты, относящиеся к количеству тандемных повторов:

● Известно, что если рассматривать только примитивные тандемные повторы (т.е. такие, половинки которых не

являются кратными chuỗiми), то их количество в любой строке —

.

● Если кодировать тандемные повторы тройками чисел (называемыми тройками Крочемора (Crochemore))

(где

— позиция начала,

— длина повторяющейся подстроки,

— количество повторов), то все тандемные повторы

любой строки можно вывести с помощью

таких троек. (Именно такой результат получается на

Đầu raе Thuật toánа Крочемора нахождения всех тандемных повторов.)

● Строки Фибоначчи, определяемые следующим образом:

являются "сильно" периодичными.

number тандемных повторов в

-ой строке Фибоначчи длины

, даже сжатых с помощью троек Крочемора,

составляет

. number примитивных тандемных повторов в chuỗiх Фибоначчи — также имеет порядок .

Thuật toán Мейна-Лоренца

Идея Thuật toánа Мейна-Лоренца довольно стандартна: это Thuật toán "разделяй-и-властвуй". Кратко он заключается в том, что исходная chuỗi разбивается пополам, Lời giải запускается от каждой из двух половинок по отдельности (тем самым мы найдём все тандемные повторы, располагающиеся только в первой или только во второй половинке). Дальше идёт самая сложная часть — это нахождение тандемных повторов, начинающихся в первой половине и заканчивающихся во второй (назовём такие тандемные повторы для удобства пересекающими). Как именно это делается — и есть сама суть Thuật toánа Мейна-Лоренца; это мы подробно опишем ниже. Asymptotic complexity Thuật toánа "разделяй-и-властвуй" хорошо исследована. В частности, для нас важно, что если мы

научимся искать пересекающие тандемные повторы в строке длины

за

, то итоговая Asymptotic complexity всего

Thuật toánа получится

.

Поиск пересекающих тандемных повторов

Итак, Thuật toán Мейна-Лоренца свёлся к тому, чтобы по заданной строке

научиться искать все пересекающие

тандемные повторы, т.е. такие, которые начинаются в первой половине строки, а заканчиваются — во второй.

Обозначим через

и

две половинки строки

:

(их длины Ví dụно равны длине строки

, делённой пополам).

Правые и левые тандемные повторы

Рассмотрим произвольный тандемный повтор и посмотрим на его средний символ (точнее, на тот символ, с которого начинается вторая половинка тандема; т.е. если тандемный повтор — это substring

, то

средним символом будет

. Тогда назовём тандемный повтор левым или правым в зависимости от того, где находится этот символ —

в строке

или в строке

. (Можно сказать и так: тандемный повтор называется левым, если большая его часть лежит

в левой половине строки

; иначе — тандемный повтор называется правым.)

Научимся искать все левые тандемные повторы; для правых всё будет аналогично.

Центральная позиция

тандемного повтора

Обозначим длину искомого левого тандемного повтора через

(т.е. длина каждой половинки тандемного повтора —

это

). Рассмотрим первый символ тандемного повтора, попадающий в строку

(он стоит в строке

в

позиции

). Он совпадает с символом, стоящим на

позиций раньше него; обозначим эту позицию

через

.

Искать все тандемные повторы мы будем, перебирая эту позицию

: т.е.

найдём сначала все тандемные повторы с одним значением

, затем с другим значением, и т.д. — перебирая

все возможные значения

от

до

. НаVí dụ, рассмотрим такую строку:

(символ вертикальной черты разделяет две половинки

и

)

Тандемный повтор

, содержащийся в этой строке, будет обнаружен, когда мы будем просматривать

значение

— потому что именно в позиции

стоит символ 'a', совпадающий с первым символом

тандемного повтора, попавшим в половинку

.

Критерий наличия тандемного повтора с заданным центром

Итак, мы должны научиться для зафиксированного значения

быстро искать все тандемные

повторы, соответствующие ему. Получаем такую схему (для абстрактной строки, в которой содержится тандемный повтор ):

Здесь через

и

мы обозначили длины двух кусочков тандемного повтора:

— это длина части тандемного повтора

до позиции

, а

— это длина части тандемного повтора от

до конца половинки тандемного

повтора. Таким образом,

— это длина тандемного повтора. Взглянув на эту картинку, можно понять, что необходимое и достаточное Đề bài того, что с центром

в позиции

находится тандемный повтор длины

,

является следующее Đề bài:

● Пусть

— это наибольшее number такое, что

символов перед позицией

совпадают с последними

символами строки

:

● Пусть

— это наибольшее number такое, что

символов, начиная с позиции

, совпадают с первыми

символами строки

:

● Тогда должно выполняться:

Этот критерий можно переформулировать таким образом. Зафиксируем конкретное значение , тогда:

● Все тандемные повторы, которые мы будем сейчас обнаруживать, будут иметь длину

. Однако таких тандемных повторов может быть несколько: всё зависит от выбора длин кусочков

и

.

● Найдём

и

, как было описано выше.

● Тогда подходящими будут являться тандемные повторы, для которых длины кусочков

и

удовлетворяют условиям:

Thuật toán нахождения длин

и

Итак, вся Bài toán сводится к быстрому вычислению длин

и

для каждого значения

. Напомним их определения:

— максимальное неотрицательное number, для которого выполнено:

— максимальное неотрицательное number, для которого выполнено:

На оба этих запроса можно отвечать за

, используя Thuật toán нахождения Z-функции:

● Для быстрого нахождения значений

заранее посчитаем Z-функцию для строки

(т.е. строки

, выписанной в

обратном порядке).

Тогда значение

для конкретного

будет просто равно соответствующему значению mảngа Z-функции.

● Для быстрого нахождения значений

заранее посчитаем Z-функцию для строки

(т.е. строки

, приписанной к строке

через символ-разделитель).

Опять же, значение

для конкретного

надо будет просто взять из соответствующего elementа Z-функции.

Поиск правых тандемных повторов

До этого момента мы работали только с левыми тандемными повторами. Чтобы искать правые тандемные повторы, надо действовать аналогично: мы определяем центр

как

символ, соответствующий последнему символу тандемного повтора, попавшему в первую строку.

Тогда длина

будет определяться как наибольшее number символов до позиции

включительно, совпадающих

с последними символами строки

. Длина

будет определяться как максимальное number символов, начиная

с

, совпадающих с первыми символами строки

.

Таким образом, для быстрого нахождения

и

надо будет посчитать заранее Z-функцию для строк

и

соответственно. После этого, перебирая конкретное значение

, мы по тому же самому критерию будем

находить все правые тандемные повторы.

Asymptotic complexity

Асмиптотика Thuật toánа Мейна-Лоренца составит, таким образом,

: поскольку этот Thuật toán

представляет собой Thuật toán "разделяй-и-властвуй", каждый рекурсивный запуск которого работает за время, линейное относительно длины строки: для четырёх строк за линейное время ищется их Z-функция, а затем

перебирается значение

и выводятся все группы обнаруженных тандемных повторов. Тандемные повторы обнаруживаются Thuật toánом Мейна-Лоренца в виде своеобразных групп: таких

четвёрок

, каждая из которых обозначает группу тандемных повторов с длиной

, центром

и

с всевозможными длинами кусочков

и

, удовлетворяющими условиям:

Cài đặt

Приведём реализацию Thuật toánа Мейна-Лоренца, которая за время

находит все тандемные

повторы данной строки в сжатом виде (в виде групп, описываемых четвёрками чисел).

В целях демонстрации обнаруженные тандемные повторы за время

"разжимаются" и выводятся по

отдельности. Этот вывод при решении реальных задач легко будет заменить на какие-то другие, более эффективные, действия, наVí dụ, на поиск наидлиннейшего тандемного повтора или подсчёт количества тандемных повторов.

vector<int> z_function (const string & s) {

int n = (int) s.length();

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;

}

void output_tandem (const string & s, int shift, bool left, int cntr, int

l, int l1, int l2) {

int pos;
if (left)

pos = cntr-l1;

else

pos = cntr-l1-l2-l1+1;

cout << "[" << shift + pos << ".." << shift + pos+2*l-1 << "] = " <<

s.substr (pos, 2*l) << endl;

}

void output_tandems (const string & s, int shift, bool left, int cntr, int

l, int k1, int k2) {

for (int l1=1; l1<=l; ++l1) {
if (left && l1 == l)  break;
if (l1 <= k1 && l-l1 <= k2)

output_tandem (s, shift, left, cntr, l, l1, l-l1);

} }

inline int get_z (const vector<int> & z, int i) {

return 0<=i && i<(int)z.size() ? z[i] : 0;

}

void find_tandems (string s, int shift = 0) {

int n = (int) s.length();
if (n == 1)  return;
int nu = n/2,  nv = n-nu;

string u = s.substr (0, nu),

v = s.substr (nu);

string ru = string (u.rbegin(), u.rend()),

rv = string (v.rbegin(), v.rend());

find_tandems (u, shift);

find_tandems (v, shift + nu);

vector<int> z1 = z_function (ru),

z2 = z_function (v + '#' + u),

z3 = z_function (ru + '#' + rv),

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)

output_tandems (s, shift, cntr<nu, cntr, l, k1, k2);

} }

References

● Michael Main, Richard J. Lorentz. An O (n log n) Algorithm for Finding All Repetitions in a

String [1982]

● Bill Smyth. Computing Patterns in Strings [2003]

● Билл Смит. Методы и Thuật toánы вычислений на chuỗiх [2006]

C# lời giải

bản nháp tự động, xem lại trước khi gửi
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    List<int> z_function (const string & s) {
            int n = (int) s.length();
            List<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;
    }
    void output_tandem (const string & s, int shift, bool left, int cntr, int
    l, int l1, int l2) {
            int pos;
            if (left)
                    pos = cntr-l1;
            else
                    pos = cntr-l1-l2-l1+1;
            Console.WriteLine( "[" << shift + pos << ".." << shift + pos+2*l-1 << "] = " <<
    s.substr (pos, 2*l) << endl;
    }
    void output_tandems (const string & s, int shift, bool left, int cntr, int
    l, int k1, int k2) {
            for (int l1=1; l1<=l; ++l1) {
                    if (left && l1 == l)  break;
                    if (l1 <= k1 && l-l1 <= k2)
                            output_tandem (s, shift, left, cntr, l, l1, l-l1);
            }
    }
    inline int get_z (const List<int> & z, int i) {
            return 0<=i && i<(int)z.size() ? z[i] : 0;
    }
    void find_tandems (string s, int shift = 0) {
            int n = (int) s.length();
            if (n == 1)  return;
            int nu = n/2,  nv = n-nu;
            string u = s.substr (0, nu),
                    v = s.substr (nu);
            string ru = string (u.rbegin(), u.rend()),
                    rv = string (v.rbegin(), v.rend());
            find_tandems (u, shift);
            find_tandems (v, shift + nu);
            List<int> z1 = z_function (ru),
                    z2 = z_function (v + '#' + u),
                    z3 = z_function (ru + '#' + rv),
                    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)
                            output_tandems (s, shift, cntr<nu, cntr, l, k1, k2);
            }
    }
}

C++ lời giải

đã khớp/gốc
vector<int> z_function (const string & s) {
        int n = (int) s.length();
        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;
}
void output_tandem (const string & s, int shift, bool left, int cntr, int
l, int l1, int l2) {
        int pos;
        if (left)
                pos = cntr-l1;
        else
                pos = cntr-l1-l2-l1+1;
        cout << "[" << shift + pos << ".." << shift + pos+2*l-1 << "] = " <<
s.substr (pos, 2*l) << endl;
}
void output_tandems (const string & s, int shift, bool left, int cntr, int
l, int k1, int k2) {
        for (int l1=1; l1<=l; ++l1) {
                if (left && l1 == l)  break;
                if (l1 <= k1 && l-l1 <= k2)
                        output_tandem (s, shift, left, cntr, l, l1, l-l1);
        }
}
inline int get_z (const vector<int> & z, int i) {
        return 0<=i && i<(int)z.size() ? z[i] : 0;
}
void find_tandems (string s, int shift = 0) {
        int n = (int) s.length();
        if (n == 1)  return;
        int nu = n/2,  nv = n-nu;
        string u = s.substr (0, nu),
                v = s.substr (nu);
        string ru = string (u.rbegin(), u.rend()),
                rv = string (v.rbegin(), v.rend());
        find_tandems (u, shift);
        find_tandems (v, shift + nu);
        vector<int> z1 = z_function (ru),
                z2 = z_function (v + '#' + u),
                z3 = z_function (ru + '#' + rv),
                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)
                        output_tandems (s, shift, cntr<nu, cntr, l, k1, k2);
        }
}

Java lời giải

bản nháp tự động, xem lại trước khi gửi
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    ArrayList<Integer> z_function (const string & s) {
            int n = (int) s.length();
            ArrayList<Integer> 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;
    }
    void output_tandem (const string & s, int shift, boolean left, int cntr, int
    l, int l1, int l2) {
            int pos;
            if (left)
                    pos = cntr-l1;
            else
                    pos = cntr-l1-l2-l1+1;
            System.out.println( "[" << shift + pos << ".." << shift + pos+2*l-1 << "] = " <<
    s.substr (pos, 2*l) << endl;
    }
    void output_tandems (const string & s, int shift, boolean left, int cntr, int
    l, int k1, int k2) {
            for (int l1=1; l1<=l; ++l1) {
                    if (left && l1 == l)  break;
                    if (l1 <= k1 && l-l1 <= k2)
                            output_tandem (s, shift, left, cntr, l, l1, l-l1);
            }
    }
    inline int get_z (const ArrayList<Integer> & z, int i) {
            return 0<=i && i<(int)z.size() ? z[i] : 0;
    }
    void find_tandems (string s, int shift = 0) {
            int n = (int) s.length();
            if (n == 1)  return;
            int nu = n/2,  nv = n-nu;
            string u = s.substr (0, nu),
                    v = s.substr (nu);
            string ru = string (u.rbegin(), u.rend()),
                    rv = string (v.rbegin(), v.rend());
            find_tandems (u, shift);
            find_tandems (v, shift + nu);
            ArrayList<Integer> z1 = z_function (ru),
                    z2 = z_function (v + '#' + u),
                    z3 = z_function (ru + '#' + rv),
                    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)
                            output_tandems (s, shift, cntr<nu, cntr, l, k1, k2);
            }
    }
}

Материал разбит как Thuật toánическая Bài toán: изучить постановку, понять асимптотику и реализовать Thuật toán на выбранном языке.

Vacancies for this task

việc làm đang hoạt động with overlapping task tags are đã hiển thị.

Tất cả việc làm
Chưa có việc làm đang hoạt động.