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 표시됨.

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