E103. Suffix array

e-maxx algorithm original: C/C++ #algorithm #array #emaxx #string #suffix-structure
선택한 UI 언어에 맞게 문제 텍스트를 러시아어에서 번역합니다. 코드는 변경하지 않습니다.

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

Дана 문자열

длины

.

-ым суффиксом строки называется substring

,

.

Тогда суффиксным 배열ом строки

называется перестановка индексов суффиксов

,

, которая задаёт порядок суффиксов в порядке лексико그래프ической

сортировки. Иными словами, нужно выполнить сортировку всех суффиксов заданной строки.

На예제, для строки

Suffix array будет равен:

Построение за

Строго говоря, описываемый ниже 알고리즘 будет выполнять сортировку не суффиксов, а циклических сдвигов строки. Однако из этого 알고리즘а легко получить и 알고리즘 сортировки суффиксов: достаточно приписать в конец строки произвольный символ, который заведомо меньше любого символа, из которого может состоять 문자열 (на예제, это может быть доллар или шарп; в языке C в этих целях можно использовать уже имеющийся нулевой символ). Сразу заметим, что поскольку мы сортируем циклические сдвиги, то и подстроки мы будем

рассматривать циклические: под подстрокой

, когда

, понимается

substring

. Кроме того, предварительно все индексы берутся по модулю длины строки (в целях упрощения формул я буду опускать явные взятия индексов по модулю).

Рассматриваемый нами 알고리즘 состоит из 예제но

фаз. На

-ой фазе (

)

сортируются циклические подстроки длины

. На последней,

-ой фазе, будут сортироваться подстроки

длины

, что эквивалентно сортировке циклических сдвигов.

На каждой фазе 알고리즘 помимо перестановки

индексов циклических подстрок будет

поддерживать для каждой циклической подстроки, начинающейся в позиции

с длиной

, номер

класса эквивалентности, которому эта substring принадлежит. В самом деле, среди подстрок могут быть одинаковые, и 알고리즘у понадобится информация об этом. Кроме того, номера

классов эквивалентности

будем давать таким образом, чтобы они сохраняли и информацию о порядке: если один суффикс меньше другого, то и номер класса он должен получить меньший. Классы будем для удобства нумеровать с нуля. Количество

классов эквивалентности будем хранить в переменной

.

Приведём 예제. Рассмотрим строку

. Значения 배열ов

и

на каждой стадии с нулевой

по вторую таковы:

Стоит отметить, что в 배열е

возможны неоднозначности. На예제, на нулевой фазе 배열 мог

равняться: . То, какой именно вариант получится, зависит от конкретной реализации 알고리즘а, но

все варианты одинаково правильны. В то же время, в 배열е

никаких неоднозначностей быть не могло. Перейдём теперь к построению 알고리즘а. 입력ные данные:

char *s; // 입력ная 문자열

int n; // длина строки

// константы

const int maxlen = ...; // максимальная длина строки

const int alphabet = 256; // размер алфавита, <= maxlen

На нулевой фазе мы должны отсортировать циклические подстроки длины

, т.е. отдельные символы строки,

и разделить их на классы эквивалентности (просто одинаковые символы должны быть отнесены к одному классу эквивалентности). Это можно сделать тривиально, на예제, сортировкой подсчётом. Для каждого символа посчитаем, сколько раз он встретился. Потом по этой информации восстановим 배열

. После

этого, проходом по 배열у

и сравнением символов, строится 배열

.

int p[maxlen], cnt[maxlen], c[maxlen];

memset (cnt, 0, alphabet * sizeof(int));

for (int i=0; i<n; ++i)

++cnt[s[i]];

for (int i=1; i<alphabet; ++i)

cnt[i] += cnt[i-1];

for (int i=0; i<n; ++i)

p[--cnt[s[i]]] = i;

c[p[0]] = 0;

int classes = 1;
for (int i=1; i<n; ++i) {
if (s[p[i]] != s[p[i-1]])  ++classes;

c[p[i]] = classes-1;

}

Далее, пусть мы выполнили

-ю фазу (т.е. вычислили значения 배열ов

и

для неё), теперь научимся

за

выполнять следующую,

-ю, фазу. Поскольку фаз всего

, это даст нам

требуемый 알고리즘 с временем

.

Для этого заметим, что циклическая substring длины

состоит из двух подстрок длины

, которые мы

можем сравнивать между собой за

, используя информацию с предыдущей фазы — номера

классов эквивалентности. Таким образом, для подстроки длины

, начинающейся в позиции

, вся

необходимая информация содержится в паре чисел

(повторимся, мы используем 배열

с предыдущей фазы). Это даёт нам весьма простое 해법: отсортировать подстроки длины

просто по этим парам чисел,

это и даст нам требуемый порядок, т.е. 배열

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

, нас не устроит — это даст 알고리즘 построения суффиксного 배열а с временем

(зато этот 알고리즘 несколько проще в написании, чем описываемый ниже). Как быстро выполнить такую сортировку пар? Поскольку elementы пар не превосходят

, то можно выполнить

сортировку подсчётом. Однако для достижения лучшей скрытой в асимптотике константы вместо сортировки пар придём к сортировке просто чисел. Воспользуемся здесь приёмом, на котором основана так называемая цифровая сортировка: чтобы отсортировать пары, отсортируем их сначала по вторым elementам, а затем — по первым elementам (но уже обязательно стабильной сортировкой, т.е. не нарушающей относительного порядка elementов при равенстве). Однако отдельно вторые elementы уже упорядочены — этот порядок задан в 배열е от предыдущей фазы. Тогда, чтобы упорядочить пары по вторым elementам, надо просто от каждого elementа 배열а

отнять

это даст нам порядок сортировки пар по вторым elementам (ведь

даёт упорядочение подстрок длины

, и

при переходе к строке вдвое большей длины эти подстроки становятся их вторыми половинками, поэтому от позиции второй половинки отнимается длина первой половинки). Таким образом, с помощью всего лишь вычитаний от elementов 배열а

мы производим сортировку по

вторым elementам пар. Теперь надо произвести стабильную сортировку по первым elementам пар, её уже

можно выполнить за

с помощью сортировки подсчётом.

Осталось только пересчитать номера

классов эквивалентности, но их уже легко получить, просто пройдя по

полученной новой перестановке

и сравнивая соседние elementы (опять же, сравнивая как пары двух чисел). Приведём реализацию выполнения всех фаз 알고리즘а, кроме нулевой. Вводятся дополнительно

временные 배열ы

и

(

— содержит перестановку в порядке сортировки по вторым elementам пар,

— новые номера классов эквивалентности).

int pn[maxlen], cn[maxlen];
for (int h=0; (1<<h)<n; ++h) {
for (int i=0; i<n; ++i) {

pn[i] = p[i] - (1<<h);

if (pn[i] < 0)  pn[i] += n;

}

memset (cnt, 0, classes * sizeof(int));

for (int i=0; i<n; ++i)

++cnt[c[pn[i]]];

for (int i=1; i<classes; ++i)

cnt[i] += cnt[i-1];

for (int i=n-1; i>=0; --i)

p[--cnt[c[pn[i]]]] = pn[i];

cn[p[0]] = 0;

classes = 1;

for (int i=1; i<n; ++i) {
int mid1 = (p[i] + (1<<h)) % n,  mid2 = (p[i-1] + (1<<h)) % n;
if (c[p[i]] != c[p[i-1]] || c[mid1] != c[mid2])

++classes;

cn[p[i]] = classes-1;

}

memcpy (c, cn, n * sizeof(int));

}

Этот 알고리즘 требует

времени и

памяти. Впрочем, если учитывать ещё размер

алфавита,

то 실행 시간 становится

, а размер памяти —

.

Applications

Нахождение наименьшего циклического сдвига строки

Вышеописанный 알고리즘 производит сортировку циклических сдвигов (если к строке не приписывать доллар), а

потому

даст искомую позицию наименьшего циклического сдвига. 실행 시간 — .

Поиск подстроки в строке

Пусть it is required в тексте

искать строку

в режиме онлайн (т.е. заранее строку

нужно считать неизвестной).

Построим Suffix array для текста

за

. Теперь подстроку

будем искать следующим

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

. Поскольку суффиксы у

нас упорядочены (это даёт нам Suffix array), то подстроку

можно искать бинарным поиском по

суффиксам строки. Сравнение текущего суффикса и подстроки

внутри бинарного поиска можно производить

тривиально, за

. Тогда Asymptotic complexity поиска подстроки в тексте становится

.

Сравнение двух подстрок строки

it is required по заданной строке

, произведя некоторый её препроцессинг, научиться за

отвечать на

запросы сравнения двух произвольных подстрок (т.е. проверка, что первая substring равна/меньше/больше второй).

Построим Suffix array за

, при этом сохраним промежуточные результаты: нам

понадобятся 배열ы

от каждой фазы. Поэтому памяти поit is required тоже

.

Используя эту информацию, мы можем за

сравнивать любые две подстроки длины, равной степени двойки: для этого достаточно сравнить номера классов эквивалентности из соответствующей фазы. Теперь надо обобщить этот способ на подстроки произвольной длины. Пусть теперь поступил очередной запрос сравнения двух подстрок длины

с началами в индексах

и

. Найдём наибольшую длину блока, помещающегося внутри подстроки такой длины, т.е. наибольшее

такое, что

. Тогда сравнение двух подстрок можно заменить сравнением двух пар перекрывающихся блоков длины

: сначала надо сравнить два блока, начинающихся в позициях

и

, а при равенстве — сравнить два

блока, заканчивающихся в позициях

и

:

Таким образом, 구현 получается 예제но такой (здесь считается, что вызывающая процедура сама вычисляет , поскольку сделать это за константное время не так легко (по-видимому, быстрее всего — предпосчётом), но в любом случае это не имеет отношения к применению суффиксного 배열а):

int compare (int i, int j, int l, int k) {

pair<int,int> a = make_pair (c[k][i], c[k][i+l-(1<<(k-1))]);

pair<int,int> b = make_pair (c[k][j], c[k][j+l-(1<<(k-1))]);

return a == b ? 0 : a < b ? -1 : 1;

}

Наибольший общий префикс двух подстрок: способ с

дополнительной памятью

it is required по заданной строке

, произведя некоторый её препроцессинг, научиться за

отвечать на

запросы наибольшего общего префикса (longest common prefix, lcp) для двух произвольных суффиксов с позициями

и

.

Способ, описываемый здесь, требует

дополнительной памяти; другой способ, использующий

линейный объём памяти, но неконстантное время ответа на запрос, описан в следующем разделе.

Построим Suffix array за

, при этом сохраним промежуточные результаты: нам

понадобятся 배열ы

от каждой фазы. Поэтому памяти поit is required тоже

.

Пусть теперь поступил очередной запрос: пара индексов

и

. Воспользуемся тем, что мы можем за

сравнивать любые две подстроки длины, являющейся степенью двойки. Для этого будем перебирать степень двойки (от большей к меньшей), и для текущей степени проверять: если подстроки такой длины совпадают, то к ответу прибавить эту степень двойки, а наибольший общий префикс продолжим искать справа от одинаковой части, т.е. к

и

надо прибавить текущую степень двойки. 구현:

int lcp (int i, int j) {
int ans = 0;
for (int k=log_n; k>=0; --k)
if (c[k][i] == c[k][j]) {

ans += 1<<k;

i += 1<<k;

j += 1<<k;

}

return ans;

}

Здесь через

обозначена константа, равная логарифму

по основанию 2, округлённому вниз.

Наибольший общий префикс двух подстрок: способ

без дополнительной памяти. Наибольший общий префикс

двух соседних суффиксов

it is required по заданной строке

, произведя некоторый её препроцессинг, научиться отвечать на запросы наибольшего общего префикса (longest common prefix, lcp) для двух произвольных суффиксов с позициями

и

. В отличие от предыдущего метода, описываемый здесь будет выполнять препроцессинг строки за

времени с

памяти. Результатом этого препроцессинга будет являться 배열 (который сам по себе является важным источником информации о строке, и потому использоваться для решения других задач). Ответы же на запрос будут производиться как результат выполнения запроса RMQ (минимум на отрезке, range minimum query) в этом 배열е, поэтому при разных 구현х можно получить как логарифмическое, так и константное времена работы. Базой для этого 알고리즘а является следующая идея: найдём каким-нибудь образом наибольшие общие префиксы для каждой соседней в порядке сортировки пары суффиксов. Иными словами, построим

배열

, где

равен наибольшему общему префиксу суффиксов

и

. Этот

배열 даст нам ответ для любых двух соседних суффиксов строки. Тогда ответ для любых двух суффиксов, не обязательно соседних, можно получить по этому 배열у. В самом деле, пусть поступил запрос с некоторыми

номерами суффиксов

и

. Найдём эти индексы в суффиксном 배열е, т.е. пусть

и

— их позиции в 배열е

(упорядочим их, т.е. пусть

). Тогда ответом на данный запрос будет минимум в 배열е

, взятый

на отрезке

. В самом деле, переход от суффикса

к суффиксу

можно заменить целой

цепочкой переходов, начинающейся с суффикса

и заканчивающейся в суффиксе

, но включающей в себя

все промежуточные суффиксы, находящиеся в порядке сортировки между ними.

Таким образом, если мы имеем такой 배열

, то ответ на любой запрос наибольшего общего префикса сводится

к запросу минимума на отрезке 배열а

. Эта классическая 문제 минимума на отрезке (range

minimum query, RMQ) имеет множество решений с различными Asymptotic complexityми, описанные здесь.

Итак, основная наша 문제 — построение этого 배열а

. Строить его мы будем по ходу 알고리즘а

построения суффиксного 배열а: на каждой текущей итерации будем строить 배열

для циклических

подстрок текущей длины.

После нулевой итерации 배열

, очевидно, должен быть нулевым.

Пусть теперь мы выполнили

-ю итерацию, получили от неё 배열

, и должны на текущей

итерации пересчитать этот 배열, получив новое его значение

. Как мы помним, в 알고리즘е

построения суффиксного 배열а циклические подстроки длины

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

; воспользуемся этим же приёмом и для построения 배열а

. Итак, пусть на текущей итерации 알고리즘 вычисления суффиксного 배열а выполнил свою работу, нашёл

новое значение перестановки

подстрок. Будем теперь идти по этому 배열у и смотреть пары соседних подстрок:

и

,

. Разбивая каждую подстроку пополам, мы получаем две различных ситуации:

1) первые половинки подстрок в позициях

и

различаются, и 2) первые половинки совпадают

(напомним, такое сравнение можно легко производить, просто сравнивая номера классов

с предыдущей

итерации). Рассмотрим каждый из этих случаев отдельно. 1) Первые половинки подстрок различались. Заметим, что тогда на предыдущем шаге эти первые половинки необходимо были соседними. В самом деле, классы эквивалентности не могли исчезать (а могут только

появляться), поэтому все различные подстроки длины

дадут (в качестве первых половинок) на текущей

итерации различные подстроки длины

, и в том же порядке. Таким образом, для определения

в этом

случае надо просто взять соответствующее значение из 배열а

. 2) Первые половинки совпадали. Тогда вторые половинки могли как совпадать, так и различаться; при этом, если они различаются, то они совсем не обязательно должны были быть соседними на предыдущей итерации. Поэтому в

этом случае нет простого способа определить

. Для его определения надо поступить так же, как мы и

собираемся потом вычислять наибольший общий префикс для любых двух суффиксов: надо выполнить запрос

минимума (RMQ) на соответствующем отрезке 배열а

. Оценим асимптотику такого 알고리즘а. Как мы видели при разборе этих двух случаев, только второй случай даёт увеличение числа классов эквивалентности. Иными словами, можно говорить о том, что каждый новый класс эквивалентности появляется вместе с одним запросом RMQ. Поскольку всего классов эквивалентности может

быть до

, то и искать минимум мы должны за асимптотику

. А для этого надо использовать уже какую-

то структуру данных для минимума на отрезке; эту структуру данных надо будет строить заново на каждой

итерации (которых всего

). Хорошим вариантом структуры данных будет Segment tree: его

можно построить за

, а потом выполнять запросы за

, что как раз и даёт нам итоговую

асимптотику

. 구현:

int lcp[maxlen], lcpn[maxlen], lpos[maxlen], rpos[maxlen];

memset (lcp, 0, sizeof lcp);

for (int h=0; (1<<h)<n; ++h) {
for (int i=0; i<n; ++i)

rpos[c[p[i]]] = i;

for (int i=n-1; i>=0; --i)

lpos[c[p[i]]] = i;

... все действия по построению суфф. 배열а, кроме последней

строки (memcpy) ...

rmq_build (lcp, n-1);

for (int i=0; i<n-1; ++i) {
int a = p[i],  b = p[i+1];
if (c[a] != c[b])

lcpn[i] = lcp[rpos[c[a]]];

else {

int aa = (a + (1<<h)) % n,  bb = (b + (1<<h)) % n;

lcpn[i] = (1<<h) + rmq (lpos[c[aa]], rpos[c[bb]]-1);

lcpn[i] = min (n, lcpn[i]);

} }

memcpy (lcp, lcpn, (n-1) * sizeof(int));

memcpy (c, cn, n * sizeof(int));

}

Здесь помимо 배열а

вводится временный 배열

с его новым значением. Также поддерживается

배열

, который для каждой подстроки хранит её позицию в перестановке

. Функция

некоторая функция, строящая структуру данных для минимума по 배열у-первому аргументу, размер его

передаётся вторым аргументом. Функция

returns минимум на отрезке: с первого аргумента по

второй включительно. Из самого 알고리즘а построения суффиксного 배열а пришлось только вынести копирование 배열а

, поскольку

во время вычисления

нам понадобятся старые значения этого 배열а. Стоит отметить, что наша 구현 находит длину общего префикса для циклических подстрок, в то время как на практике чаще бывает нужной длина общего префикса для суффиксов в их обычном понимании. В

этом случае надо просто ограничить значения

по окончании работы 알고리즘а:

for (int i=0; i<n-1; ++i)

lcp[i] = min (lcp[i], min (n-p[i], n-p[i+1]));

Для любых двух суффиксов длину их наибольшего общего префикса теперь можно find как минимум

на соответствующем отрезке 배열а

:

for (int i=0; i<n; ++i)

pos[p[i]] = i;

rmq_build (lcp, n-1);

... поступил запрос (i,j) на нахождение LCP ...

int result = rmq (min(i,j), max(i,j)-1);

Количество различных подстрок

Выполним препроцессинг, описанный в предыдущем разделе: за

времени и

памяти мы

для каждой пары соседних в порядке сортировки суффиксов найдём длину их наибольшего общего префикса.

Найдём теперь по этой информа

...

C# 해법

자동 초안, 제출 전 검토
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.
    char *s; // входная строка
    int n; // длина строки
    // константы
    const int maxlen = ...; // максимальная длина строки
    const int alphabet = 256; // размер алфавита, <= maxlen
    int p[maxlen], cnt[maxlen], c[maxlen];
    memset (cnt, 0, alphabet * sizeof(int));
    for (int i=0; i<n; ++i)
            ++cnt[s[i]];
    for (int i=1; i<alphabet; ++i)
            cnt[i] += cnt[i-1];
    for (int i=0; i<n; ++i)
            p[--cnt[s[i]]] = i;
    c[p[0]] = 0;
    int classes = 1;
    for (int i=1; i<n; ++i) {
            if (s[p[i]] != s[p[i-1]])  ++classes;
            c[p[i]] = classes-1;
    }
    int pn[maxlen], cn[maxlen];
    for (int h=0; (1<<h)<n; ++h) {
            for (int i=0; i<n; ++i) {
                    pn[i] = p[i] - (1<<h);
                    if (pn[i] < 0)  pn[i] += n;
            }
            memset (cnt, 0, classes * sizeof(int));
            for (int i=0; i<n; ++i)
                    ++cnt[c[pn[i]]];
            for (int i=1; i<classes; ++i)
                    cnt[i] += cnt[i-1];
            for (int i=n-1; i>=0; --i)
                    p[--cnt[c[pn[i]]]] = pn[i];
            cn[p[0]] = 0;
            classes = 1;
            for (int i=1; i<n; ++i) {
                    int mid1 = (p[i] + (1<<h)) % n,  mid2 = (p[i-1] + (1<<h)) % n;
                    if (c[p[i]] != c[p[i-1]] || c[mid1] != c[mid2])
                            ++classes;
                    cn[p[i]] = classes-1;
            }
            memcpy (c, cn, n * sizeof(int));
    }
    int compare (int i, int j, int l, int k) {
            pair<int,int> a = make_pair (c[k][i], c[k][i+l-(1<<(k-1))]);
            pair<int,int> b = make_pair (c[k][j], c[k][j+l-(1<<(k-1))]);
            return a == b ? 0 : a < b ? -1 : 1;
    }
    int lcp (int i, int j) {
            int ans = 0;
            for (int k=log_n; k>=0; --k)
                    if (c[k][i] == c[k][j]) {
                            ans += 1<<k;
                            i += 1<<k;
                            j += 1<<k;
                    }
            return ans;
    }
    int lcp[maxlen], lcpn[maxlen], lpos[maxlen], rpos[maxlen];
    memset (lcp, 0, sizeof lcp);
    for (int h=0; (1<<h)<n; ++h) {
            for (int i=0; i<n; ++i)
                    rpos[c[p[i]]] = i;
            for (int i=n-1; i>=0; --i)
                    lpos[c[p[i]]] = i;
            ... все действия по построению суфф. массива, кроме последней
    строки (memcpy) ...
            rmq_build (lcp, n-1);
            for (int i=0; i<n-1; ++i) {
                    int a = p[i],  b = p[i+1];
                    if (c[a] != c[b])
                            lcpn[i] = lcp[rpos[c[a]]];
                    else {
                            int aa = (a + (1<<h)) % n,  bb = (b + (1<<h)) % n;
                            lcpn[i] = (1<<h) + rmq (lpos[c[aa]], rpos[c[bb]]-1);
                            lcpn[i] = min (n, lcpn[i]);
                    }
            }
            memcpy (lcp, lcpn, (n-1) * sizeof(int));
            memcpy (c, cn, n * sizeof(int));
    }
    for (int i=0; i<n-1; ++i)
            lcp[i] = min (lcp[i], min (n-p[i], n-p[i+1]));
    for (int i=0; i<n; ++i)
            pos[p[i]] = i;
    rmq_build (lcp, n-1);
    ... поступил запрос (i,j) на нахождение LCP ...
    int result = rmq (min(i,j), max(i,j)-1);
}

C++ 해법

매칭됨/원본
char *s; // входная строка
int n; // длина строки
// константы
const int maxlen = ...; // максимальная длина строки
const int alphabet = 256; // размер алфавита, <= maxlen
int p[maxlen], cnt[maxlen], c[maxlen];
memset (cnt, 0, alphabet * sizeof(int));
for (int i=0; i<n; ++i)
        ++cnt[s[i]];
for (int i=1; i<alphabet; ++i)
        cnt[i] += cnt[i-1];
for (int i=0; i<n; ++i)
        p[--cnt[s[i]]] = i;
c[p[0]] = 0;
int classes = 1;
for (int i=1; i<n; ++i) {
        if (s[p[i]] != s[p[i-1]])  ++classes;
        c[p[i]] = classes-1;
}
int pn[maxlen], cn[maxlen];
for (int h=0; (1<<h)<n; ++h) {
        for (int i=0; i<n; ++i) {
                pn[i] = p[i] - (1<<h);
                if (pn[i] < 0)  pn[i] += n;
        }
        memset (cnt, 0, classes * sizeof(int));
        for (int i=0; i<n; ++i)
                ++cnt[c[pn[i]]];
        for (int i=1; i<classes; ++i)
                cnt[i] += cnt[i-1];
        for (int i=n-1; i>=0; --i)
                p[--cnt[c[pn[i]]]] = pn[i];
        cn[p[0]] = 0;
        classes = 1;
        for (int i=1; i<n; ++i) {
                int mid1 = (p[i] + (1<<h)) % n,  mid2 = (p[i-1] + (1<<h)) % n;
                if (c[p[i]] != c[p[i-1]] || c[mid1] != c[mid2])
                        ++classes;
                cn[p[i]] = classes-1;
        }
        memcpy (c, cn, n * sizeof(int));
}
int compare (int i, int j, int l, int k) {
        pair<int,int> a = make_pair (c[k][i], c[k][i+l-(1<<(k-1))]);
        pair<int,int> b = make_pair (c[k][j], c[k][j+l-(1<<(k-1))]);
        return a == b ? 0 : a < b ? -1 : 1;
}
int lcp (int i, int j) {
        int ans = 0;
        for (int k=log_n; k>=0; --k)
                if (c[k][i] == c[k][j]) {
                        ans += 1<<k;
                        i += 1<<k;
                        j += 1<<k;
                }
        return ans;
}
int lcp[maxlen], lcpn[maxlen], lpos[maxlen], rpos[maxlen];
memset (lcp, 0, sizeof lcp);
for (int h=0; (1<<h)<n; ++h) {
        for (int i=0; i<n; ++i)
                rpos[c[p[i]]] = i;
        for (int i=n-1; i>=0; --i)
                lpos[c[p[i]]] = i;
        ... все действия по построению суфф. массива, кроме последней
строки (memcpy) ...
        rmq_build (lcp, n-1);
        for (int i=0; i<n-1; ++i) {
                int a = p[i],  b = p[i+1];
                if (c[a] != c[b])
                        lcpn[i] = lcp[rpos[c[a]]];
                else {
                        int aa = (a + (1<<h)) % n,  bb = (b + (1<<h)) % n;
                        lcpn[i] = (1<<h) + rmq (lpos[c[aa]], rpos[c[bb]]-1);
                        lcpn[i] = min (n, lcpn[i]);
                }
        }
        memcpy (lcp, lcpn, (n-1) * sizeof(int));
        memcpy (c, cn, n * sizeof(int));
}
for (int i=0; i<n-1; ++i)
        lcp[i] = min (lcp[i], min (n-p[i], n-p[i+1]));
for (int i=0; i<n; ++i)
        pos[p[i]] = i;
rmq_build (lcp, n-1);
... поступил запрос (i,j) на нахождение LCP ...
int result = rmq (min(i,j), max(i,j)-1);

Java 해법

자동 초안, 제출 전 검토
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.
    char *s; // входная строка
    int n; // длина строки
    // константы
    const int maxlen = ...; // максимальная длина строки
    const int alphabet = 256; // размер алфавита, <= maxlen
    int p[maxlen], cnt[maxlen], c[maxlen];
    memset (cnt, 0, alphabet * sizeof(int));
    for (int i=0; i<n; ++i)
            ++cnt[s[i]];
    for (int i=1; i<alphabet; ++i)
            cnt[i] += cnt[i-1];
    for (int i=0; i<n; ++i)
            p[--cnt[s[i]]] = i;
    c[p[0]] = 0;
    int classes = 1;
    for (int i=1; i<n; ++i) {
            if (s[p[i]] != s[p[i-1]])  ++classes;
            c[p[i]] = classes-1;
    }
    int pn[maxlen], cn[maxlen];
    for (int h=0; (1<<h)<n; ++h) {
            for (int i=0; i<n; ++i) {
                    pn[i] = p[i] - (1<<h);
                    if (pn[i] < 0)  pn[i] += n;
            }
            memset (cnt, 0, classes * sizeof(int));
            for (int i=0; i<n; ++i)
                    ++cnt[c[pn[i]]];
            for (int i=1; i<classes; ++i)
                    cnt[i] += cnt[i-1];
            for (int i=n-1; i>=0; --i)
                    p[--cnt[c[pn[i]]]] = pn[i];
            cn[p[0]] = 0;
            classes = 1;
            for (int i=1; i<n; ++i) {
                    int mid1 = (p[i] + (1<<h)) % n,  mid2 = (p[i-1] + (1<<h)) % n;
                    if (c[p[i]] != c[p[i-1]] || c[mid1] != c[mid2])
                            ++classes;
                    cn[p[i]] = classes-1;
            }
            memcpy (c, cn, n * sizeof(int));
    }
    int compare (int i, int j, int l, int k) {
            pair<int,int> a = make_pair (c[k][i], c[k][i+l-(1<<(k-1))]);
            pair<int,int> b = make_pair (c[k][j], c[k][j+l-(1<<(k-1))]);
            return a == b ? 0 : a < b ? -1 : 1;
    }
    int lcp (int i, int j) {
            int ans = 0;
            for (int k=log_n; k>=0; --k)
                    if (c[k][i] == c[k][j]) {
                            ans += 1<<k;
                            i += 1<<k;
                            j += 1<<k;
                    }
            return ans;
    }
    int lcp[maxlen], lcpn[maxlen], lpos[maxlen], rpos[maxlen];
    memset (lcp, 0, sizeof lcp);
    for (int h=0; (1<<h)<n; ++h) {
            for (int i=0; i<n; ++i)
                    rpos[c[p[i]]] = i;
            for (int i=n-1; i>=0; --i)
                    lpos[c[p[i]]] = i;
            ... все действия по построению суфф. массива, кроме последней
    строки (memcpy) ...
            rmq_build (lcp, n-1);
            for (int i=0; i<n-1; ++i) {
                    int a = p[i],  b = p[i+1];
                    if (c[a] != c[b])
                            lcpn[i] = lcp[rpos[c[a]]];
                    else {
                            int aa = (a + (1<<h)) % n,  bb = (b + (1<<h)) % n;
                            lcpn[i] = (1<<h) + rmq (lpos[c[aa]], rpos[c[bb]]-1);
                            lcpn[i] = min (n, lcpn[i]);
                    }
            }
            memcpy (lcp, lcpn, (n-1) * sizeof(int));
            memcpy (c, cn, n * sizeof(int));
    }
    for (int i=0; i<n-1; ++i)
            lcp[i] = min (lcp[i], min (n-p[i], n-p[i+1]));
    for (int i=0; i<n; ++i)
            pos[p[i]] = i;
    rmq_build (lcp, n-1);
    ... поступил запрос (i,j) на нахождение LCP ...
    int result = rmq (min(i,j), max(i,j)-1);
}

Материал разбит как 알고리즘ическая 문제: изучить постановку, понять асимптотику и реализовать 알고리즘 на выбранном языке.

Vacancies for this task

활성 채용 with overlapping task tags are 표시됨.

전체 채용
아직 활성 채용이 없습니다.