E103. Suffix array
Источник: 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 已显示.