I too have been looking for the time-space-optimal solution to this problem. The accepted answer by tmyklebu essentially seems to be it, but I would like to offer some explanation of what it’s actually about and some further findings.
First, this question by me proposes a seemingly promising but incorrect solution, with notes on why it’s incorrect: Is this algorithm correct for finding period of a string?
In general, the problem «find the period» is equivalent to «find the pattern within itself» (in some sense, «strstr(x+1,x)
«), but with no constraints matching past its end. This means that you can find the period by taking any left-to-right string matching algorith, and applying it to itself, considering a partial match that hits the end of the haystack/text as a match, and the time and space requirements are the same as those of whatever string matching algorithm you use.
The approach cited in tmyklebu’s answer is essentially applying this principle to String Matching on Ordered Alphabets, also explained here. Another time-space-optimal solution should be possible using the GS algorithm.
The fairly well-known and simple Two Way algorithm (also explained here) unfortunately is not a solution because it’s not left-to-right. In particular, the advancement after a mismatch in the left factor depends on the right factor having been a match, and the impossibility of another match misaligned with the right factor modulo the right factor’s period. When searching for the pattern within itself and disregarding anything past the end, we can’t conclude anything about how soon the next right-factor match could occur (part or all of the right factor may have shifted past the end of the pattern), and therefore a shift that preserves linear time cannot be made.
Of course, if working space is available, a number of other algorithms may be used. KMP is linear-time with O(n) space, and it may be possible to adapt it to something still reasonably efficient with only logarithmic space.
Z-функция
Материал позаимствован с сайта e-maxx.ru.
Определение
Здесь и далее строки индексируются с нуля, т.е. первый символ строки имеет номер (0). Также, здесь и далее (s[i ldots j]) обозначает подстроку строки (s) от (i)-го символа до (j)-го включительно.
Пусть дана строка (s) длины (n). Тогда (Z(s)) — это массив длины (n), (i)-ый элемент которого равен наибольшему числу символов, начиная с позиции (i), совпадающих с первыми символами строки (s).
Иными словами, (z[i]) — это длина наибольшего общего префикса строки (s) и её (i)-го суффикса.
Первый элемент (Z)-функции, (z[0]), обычно считают неопределённым. В данной статье мы будем считать, что он равен нулю (хотя ни в алгоритме, ни в приведённой реализации это ничего не меняет).
Далее будет привиден алгоритм вычисления (Z)-функции за время (O(n)), а также различные применения этого алгоритма.
Примеры
Приведём для примера подсчитанную (Z)-функцию для нескольких строк:
-
«aaaaa»:
z[0] = 0, z[1] = 4, z[2] = 3, z[3] = 2, z[4] = 1.
-
«aaabaab»:
z[0] = 0, z[1] = 2, z[2] = 1, z[3] = 0, z[4] = 2, z[5] = 1, z[6] = 0.
-
«abacaba»:
z[0] = 0, z[1] = 0, z[2] = 1, z[3] = 0, z[4] = 3, z[5] = 0, z[6] = 1.
Тривиальный алгоритм
Формальное определение можно представить в виде следующей элементарной реализации за (O(n^2)):
def z_func(s, n): z = [0] * n for i in range(1, n - 1): while i + z[i] < n and s[z[i]] == s[i + z[i]]: z[i] += 1 return z
Мы просто для каждой позиции (i) перебираем ответ для неё (z[i]), начиная с нуля, и до тех пор, пока мы не обнаружим несовпадение или не дойдём до конца строки.
Разумеется, эта реализация слишком неэффективна, перейдём теперь к построению эффективного алгоритма.
Эффективный алгоритм вычисления Z-функции
Чтобы получить эффективный алгоритм, будем вычислять значения (z[i]) по очереди — от (i=1) до (n-1), и при этом постараемся при вычислении очередного значения (z[i]) максимально использовать уже вычисленные значения.
Назовём для краткости подстроку, совпадающую с префиксом строки (s), отрезком совпадения. Например, значение искомой Z-функции (z[i]) — это длина длиннейшего отрезок совпадения, начинающийся в позиции (i) (и заканчиваться он будет в позиции (i + z[i] — 1)).
Для этого будем поддерживать координаты (boldsymbol{[l;r]}) самого правого отрезка совпадения, т.е. из всех обнаруженных отрезков будем хранить тот, который оканчивается правее всего. В некотором смысле, индекс (r) — это такая граница, до которой наша строка уже была просканирована алгоритмом, а всё остальное — пока ещё не известно.
Тогда если текущий индекс, для которого мы хотим посчитать очередное значение (Z)-функции, — это (i), мы имеем один из двух вариантов:
-
(i > r) — т.е. текущая позиция лежит за пределами того, что мы уже успели обработать.
Тогда будем искать (z[i]) тривиальным алгоритмом, т.е. просто пробуя значения (z[i]=0), (z[i]=1), и т.д. Заметим, что в итоге, если (z[i]) окажется (> 0), то мы будем обязаны обновить координаты самого правого отрезка ([l; r]) — т.к. (i + z[i] — 1) гарантированно окажется больше (r).
-
(i le r) — т.е. текущая позиция лежит внутри отрезка совпадения ([l; r]).
Тогда мы можем использовать уже подсчитанные предыдущие значения (Z)-функции, чтобы проинициализировать значение (z[i]) не нулём, а каким-то возможно бОльшим числом.
Для этого заметим, что подстроки (s[l ldots r]) и (s[0 ldots r-l]) совпадают. Это означает, что в качестве начального приближения для (z[i]) можно взять соответствующее ему значение из отрезка (s[0 ldots r-l]), а именно, значение (z[i-l]).
Однако значение (z[i-l]) могло оказаться слишком большим: таким, что при применении его к позиции (i) оно «вылезет» за пределы границы (r). Этого допустить нельзя, т.к. про символы правее (r) мы ничего не знаем, и они могут отличаться от требуемых.
Приведём пример такой ситуации, на примере строки «aaaabaa».
Когда мы дойдём до последней позиции ((i=6)), текущим самым правым отрезком будет ([5;6]). Позиции (6) с учётом этого отрезка будет соответствовать позиция (6-5=1), ответ в которой равен (z[1] = 3). Очевидно, что таким значением инициализировать (z[6]) нельзя, оно совершенно некорректно. Максимум, каким значением мы могли проинициализировать — это (1), поскольку это наибольшее значение, которое не вылезает за пределы отрезка ([l;r]).
Таким образом, в качестве начального приближения для (z[i]) безопасно брать только такое выражение:
begin{equation*}
z_0[i] = min (r-i+1, z[i-l]).
end{equation*}Проинициализировав (z[i]) таким значением (z_0[i]), мы снова дальше действуем тривиальным алгоритмом — потому что после границы (r), вообще говоря, могло обнаружиться продолжение отрезка совпадение, предугадать которое одними лишь предыдущими значениями (Z)-функции мы не можем.
Таким образом, весь алгоритм представляет из себя два случая, которые фактически различаются только начальным значением (z[i]): в первом случае оно полагается равным нулю, а во втором — определяется по предыдущим значениям по указанной формуле. После этого обе ветки алгоритма сводятся к выполнению тривиального алгоритма, стартующего сразу с указанного начального значения.
Алгоритм получился весьма простым. Несмотря на то, что при каждом (i) в нём так или иначе выполняется тривиальный алгоритм — мы достигли существенного прогресса, получив алгоритм, работающий за линейное время (действительно, на каждый символ мы «посмотрим», т.е. сравним его с каким-либо предыдущим всего один раз).
Упражнение №1: (Z)-функция
Напишите (Z)-функцию. Пусть заголовком ее будет def z_func(s, n):
Упражнение №2: Поиск подстроки
Пусть даны две строки. Найти все вхождения второй строки в первую.
Упражнение №3: Количество разных подстрок
Найти число всех различных подстрок входящих в данную.
Упражнение №4: Период строки
Для данной строки (s) найти строку (p) минимальной длины, такую что (s) можно предстваить как конкатенацию одной или нескольких копий (p).
Префикс-функция. Алгоритм Кнута-Морриса-Пратта
Материал частично позаимствован с сайта тоже e-maxx.ru.
Префикс-функция. Определение
Пусть дана строка (s) длины (n). Тогда (pi(s)) — это массив длины (n), (i)-ый элемент которого ((pi[i])) определяется следующим образом: это длина наибольшего собственного суффикса подстроки (s[0 ldots i]), совпадающего с её префиксом (собственный суффикс — значит не совпадающий со всей строкой). В частности, значение (pi[0]) полагается равным нулю.
Примечение: вообще говоря, в теории множеств собственным считается не пустое подмножество, не совпдающее с самим множеством. В данной статье, для простоты суффикс и префикс нулевой длины также считаются собственными.
Математически определение префикс-функции можно записать следующим образом:
begin{equation*}
pi[i] = max_{k=0 ldots i} { k : s[0 ldots k — 1] = s[i — k + 1 ldots i]}.
end{equation*}
Например, для строки «abcabcd» префикс-функция равна: ([0, 0, 0, 1, 2, 3, 0]), что означает:
у строки "a" нет нетривиального префикса, совпадающего с суффиксом; у строки "ab" нет нетривиального префикса, совпадающего с суффиксом; у строки "abc" нет нетривиального префикса, совпадающего с суффиксом; у строки "abca" префикс длины 1 совпадает с суффиксом; у строки "abcab" префикс длины 2 совпадает с суффиксом; у строки "abcabc" префикс длины 3 совпадает с суффиксом; у строки "abcabcd" нет нетривиального префикса, совпадающего с суффиксом.
Другой пример — для строки «aabaaab» она равна: ([0, 1, 0, 1, 2, 2, 3]).
Тривиальный алгоритм
Непосредственно следуя определению, можно написать такой алгоритм вычисления префикс-функции:
def prefix_func(s, n): pi = [0] * n for i in range(n - 1): for k in range(1, i + 1): equal = True for j in range(k): if s[j] != s[i - k + 1 + j]: equal = False break if equal: pi[i] = k return pi
Как нетрудно заметить, работать он будет за (O(n^3)), что слишком медленно.
Эффективный алгоритм
Для удобства будем обозначать подстроки строки (s) следующим образом: пусть (p^k) — префикс (s) длины (k), (s^k_i) — подстрока длины (k) заканчивающаяся символом с номером (i). Напомним, что первый символ строки имеет номер (0).
Будем вычислять (pi[i]) последовательно, начиная с (pi[1]). (pi[0]) очевидно (= 0). Постараемся на (i) шаге получить решение, используя уже известную информацию, т.е. предыдущие значения (pi).
Во-первых заметим, что (pi[i]) превосходит (pi[i — 1]) не более чем на 1. Действительно, раз уж (p^{pi[i]}) = (s^{pi[i]}_i), значит и (p^{pi[i]-1}=s^{pi[i]-1}_{i-1}), а значит (pi[i — 1]) как минимум будет (pi[i] — 1). Это иллюстрирует схема (для (pi[i] = 4)):
begin{equation*}
underbrace{ overbrace{s_0 s_1 s_2}^{pi[i-1]} overbrace{s_3}^{s_3 = s_i }}_{pi[i] = 4} ldots underbrace{ overbrace{s_{i-3} s_{i-2} s_{i-1}}^{pi[i-1] >= 3} overbrace{s_i}^{s_3 = s_i} }_{pi[i] = 4}
end{equation*}
Будем рассматривать убывающую последовательность ({k_j}: p^{k_j} = s^{k_j}_{i — 1}, i > k_j, k_j > k_{j + 1}, j = 0, 1, …), т.е. собственные суффиксы строки (p^i), являющиеся одновременно ее префиксами, упорядоченные по убыванию длины. Очевидно, что первый из них, для которого выполнено (s[k_j] = s[i]) даст нам (pi[i] = k_j + 1). Осталось только понять, как можно быстро перебрать такие (k_j). Иллюстрация, в предположении что (k_{j+1} = 2):
begin{equation*}
overbrace{overbrace{s_0 s_1}^{k_{j+1}} s_2 ldots s_{k_j-1}}^{k_j} s_{k_j} ldots overbrace{s_{i-k_{j-1}} ldots overbrace{s_{i-2} s_{i-1}}^{k_{j+1}}}^{k_j} s_i
end{equation*}
begin{equation*}
ldots
end{equation*}
begin{equation*}
s_{k_j} = s_i Rightarrow pi[i] = k_j + 1, text{переходим к следующему i}
end{equation*}
begin{equation*}
s_{k_{j+1}} = s_i Rightarrow pi[i] = k_{j+1} + 1, text{переходим к следующему i}
end{equation*}
begin{equation*}
ldots
end{equation*}
По определению префикс-функции, очевидно, что (k_0 = pi[i — 1]). Пусть мы теперь знаем (k_j), найдем (k_{j+1}). (p^{k_j} = s^{k_j}_{i — 1}), значит, (p^{k_{j+1}} = s^{k_{j+1}}_{k_j — 1}), причем (p^{k_{j+1}}) максимален из всех таких собственных префиксов строки (p^{k_j}). Значит, (k_{j+1} = pi[k_j — 1]). Иллюстрация, в предположении что (k_{j+1} = 2):
begin{equation*}
overbrace{underbrace{s_0 s_1}_{k_{j+1}} s_2 ldots underbrace{s_{k_j-1} s_{k_j-1}}_{k_{j+1} = pi[k_j — 1]}}^{k_j} s_{k_j} ldots overbrace{s_{i-k_j} ldots underbrace{s_{i-2} s_{i-1}}_{k_{j+1}}}^{k_j} s_i
end{equation*}
Ясно, что последовательность (k_j) заканчивается первым получившимся нулем. Если при этом условие (s[k_j] = s[i]) так и не было удовлетворено, то очередное (pi[i] = 0).
Итак, (pi[0] = 0), далее, на каждом шагу алгоритма будем вычислять последовательность (k_j). Если для очередного (k_j) выполнено (s[k_j] = s[i]), то (pi[i] = k_j + 1), переходим к следующему (i). Если перебрали все (k_j) вплоть до нуля и совпадения нет, то (pi[i] = 0). Заметим, что дойдя до нуля совпадение тоже нужно проверить, в этом случае можно получить (pi[i] = 0 + 1 = 1).
Этот алгоритм был разработан Кнутом (Knuth) и Праттом (Pratt) и независимо от них Моррисом (Morris) в 1977 г. (как основной элемент для алгоритма поиска подстроки в строке). Легко видеть, что алгоритм имеет сложность (O(n)): действительно, сложность шага, на котором префикс-функция возрастает, т.е. (pi[i] = pi[i — 1] + 1) есть (O(1)), сложность шага на котором функция убывает есть (O(pi[i] — pi[i — 1])). Т.е. общая сложность есть (O(sum_i| pi[i] — pi[i — 1]|)). Сумма положительных приростов префикс-функции не превышает (n). А сумма отрицательных изменений не может превысить сумму положительных (иначе мы уйдем в минус). Значит сумма модулей изменений функции не превысит (2n), значит общая сложность (O(n)).
Как нетрудно заметить, этот алгоритм является онлайновым, т.е. он обрабатывает данные по ходу поступления — можно, например, считывать строку по одному символу и сразу обрабатывать этот символ, находя ответ для очередной позиции. Алгоритм требует хранения самой строки и предыдущих вычисленных значений префикс-функции, однако, как нетрудно заметить, если нам заранее известно максимальное значение, которое может принимать префикс-функция на всей строке, то достаточно будет хранить лишь на единицу большее количество первых символов строки и значений префикс-функции.
Поиск подстроки в строке. Алгоритм Кнута-Морриса-Пратта
Эта задача является классическим применением префикс-функции (и, собственно, она и была открыта в связи с этим).
Дан текст (t) и строка (s), требуется найти и вывести позиции всех вхождений строки (s) в текст (t).
Обозначим для удобства через (n) длину строки (s), а через (m) — длину текста (t).
Образуем строку (s + # + t), где символ (#) — это разделитель, который не должен нигде более встречаться. Посчитаем для этой строки префикс-функцию. Теперь рассмотрим её значения, кроме первых (n+1) (которые, как видно, относятся к строке (s) и разделителю). По определению, значение (pi[i]) показывает наидлиннейшую длину подстроки, оканчивающейся в позиции (i) и совпадающего с префиксом. Но в нашем случае это (pi[i]) — фактически длина наибольшего блока совпадения со строкой (s) и оканчивающегося в позиции (i). Больше, чем (n), эта длина быть не может, за счёт разделителя. А вот равенство (pi[i] = n) (там, где оно достигается), означает, что в позиции (i) оканчивается искомое вхождение строки (s) (только не надо забывать, что все позиции отсчитываются в склеенной строке (s+#+t)).
Таким образом, если в какой-то позиции (i) оказалось (pi[i] = n), то в позиции (i — (n + 1) — n + 1 = i — 2n) строки (t) начинается очередное вхождение строки (s) в строку (t).
Как уже упоминалось при описании алгоритма вычисления префикс-функции, если известно, что значения префикс-функции не будут превышать некоторой величины, то достаточно хранить не всю строку и префикс-функцию, а только её начало. В нашем случае это означает, что нужно хранить в памяти лишь строку (s + #) и значение префикс-функции на ней, а потом уже считывать по одному символу строку (t) и пересчитывать текущее значение префикс-функции.
Итак, алгоритм Кнута-Морриса-Пратта решает эту задачу за (O(n+m)) времени и (O(n)) памяти.
Упражнение №5: Префикс-функция
Напишите префикс-функцию. Пусть заголовком ее будет def p_func(s, n):
Упражнение №6: Поиск подстроки
Пусть даны две строки. Найти все вхождения второй строки в первую с помощью алгоритма Кнута-Морриса-Пратта.
Упражнение №7: Поиск подстроки онлайн
В первой строке ввода — число (n), количество букв в паттерне.
Во второй строке — паттерн, строка которую нужно искать в тексте.
В каждой из последующих строк — символы текста, по одному в каждой строке. Необходимо вывести позиции вхождений паттерна в текст. Длина текста заранее не известна, он может быть очень большим.
Упражнение №8: Количество разных подстрок
Найти число всех различных подстрок входящих в данную с помощью префикс-функции.
Упражнение №9: Период строки
Для данной строки (s) найти строку (p) минимальной длины, такую что (s) можно предстваить как конкатенацию одной или нескольких копий (p). Используйте префикс-функцию.
На днях столкнулся с задачей, где при обработке строки мне приходится вычислять минимальный период (длина периода делит длину строки) для каждой ее подстроки. Возможно ли это как-то делать за O(N^2) или хотя бы просто быстрее, чем за O(N^3)?
UPD: Ночью я хорошо поспал, поэтому, проснувшись, я вроде как нашел решение. Буду рад, если его кто-нибудь проверит:
- Для каждого префикса
P
исходной строкиS
будем считать ДП по длинеi - j + 1
его суффиксаS[j..i]
. - Пусть мы нашли минимальный период подстроки
S[j..i]
, который равенp = dp[j][i]
. Тогда предположим, что этот же период будет и у подстрокиS[j-p..i]
. Чтобы проверить наше предположение нужно только проверить равенство подстрокS[j-p..j-1]
иS[j..j+p-1]
, что можно сделать с помощью хэшей за O(1).
def is_eq_substr(beg1, end1, beg2, end2):
# Тут должен быть код для хешей, но писать хеши на питоне - себя не любить, так что я просто поставлю заглушку
global S
return S[beg1:end1+1] == S[beg2:end2+1]
S = 'ababab'
n = len(S)
dp = [[i - j + 1 for i in range(n)] for j in range(n)]
for i in range(n):
for j in range(i, -1, -1):
p = dp[j][i]
if j - dp[j][i] >= 0 and is_eq_substr(j - p, j - 1, j, j + p - 1):
dp[j - p][i] = p # min() использовать не нужно, так как dp[j - p][i] = i - (j - p) + 1
# или dp[j - p][i] = dp[j - p + p1][i] = p1, где p1 - p > 0, а значит p1 > p
for i in range(n):
print(' '.join([str(dp[j][i]) for j in range(i + 1)]))
Вопрос:
Я беру ввод от пользователя и его строку с определенной подстрокой, которая повторяется через всю строку. Мне нужно вывести подстроку или период ее длины AKA.
Скажите
S1 = AAAA // substring is A
S2 = ABAB // Substring is AB
S3 = ABCAB // Substring is ABC
S4 = EFIEFI // Substring is EFI
Я мог бы начать с Single char и проверить, совпадает ли он с его следующим символом, если это не так, я мог бы сделать это с двумя символами, а затем с тремя и так далее. Это было бы O (N ^ 2) algo. Мне было интересно, есть ли более элегантное решение для этого.
Лучший ответ:
Вы можете сделать это в линейном времени и постоянном дополнительном пространстве, индуктивно вычисляя период каждого префикса строки. Я не могу вспомнить детали (есть несколько вещей, которые можно понять правильно), но вы можете найти их в Разделе 13.6 “Алгоритмы текста” Крочемора и Риттера в function Per(x)
.
Ответ №1
Позвольте мне предположить, что длина строки n
не меньше, чем период p
.
Алгоритм
- Пусть
m
= 1 иS
вся строка - Возьмем
m
= m * 2- Найти следующее вхождение подстроки S [: m]
- Пусть
k
– начало следующего вхождения - Проверить, что S [: k] – это период
- если не перейти к 2.
Пример
Предположим, что у нас есть строка
CDCDFBFCDCDFDFCDCDFBFCDCDFDFCDC
Для каждой мощности m
из 2 мы находим повторения первых символов 2^m
. Затем мы распространим эту последовательность на второе вхождение. Пусть начинается с 2 ^ 1, поэтому CD
.
CDCDFBFCDCDFDFCDCDFBFCDCDFDFCDC
CDCD CDCD CDCD CDCD CD
Мы не расширяем CD
, так как следующее возникновение сразу после этого. Однако CD
не является подстрокой, которую мы ищем, поэтому возьмите следующую мощность: 2^2 = 4
и подстроку CDCD
.
CDCDFBFCDCDFDFCDCDFBFCDCDFDFCDC
CDCD CDCD
Теперь добавим нашу строку к первому повторению. Получаем
CDCDFBF
проверяем, является ли это периодическим. Это не значит, что мы идем дальше. Попробуем 2 ^ 3 = 8, поэтому CDCDFBFC
CDCDFBFCDCDFDFCDCDFBFCDCDFDFCDC
CDCDFBFC CDCDFBFC
мы пытаемся продолжить, и получим
CDCDFBFCDCDFDF
и это действительно наш период.
Я ожидаю, что это сработает в O (n log n) с некоторым алгоритмом, подобным KMP, для проверки того, где отображается данная строка. Обратите внимание, что некоторые краевые случаи все же должны быть разработаны здесь.
Интуитивно это должно сработать, но моя интуиция не удалась однажды по этой проблеме, поэтому, пожалуйста, исправьте меня, если я ошибаюсь. Я попытаюсь выяснить доказательство.
Очень приятная проблема.
Ответ №2
Вы можете построить дерево суффиксов для всей строки в линейном времени (дерево суффикса легко найти в Интернете), а затем рекурсивно вычислить и сохранить количество листьев суффикса (вхождения префикса суффикса) N (v) ниже каждого внутреннего node v дерева суффикса. Также рекурсивно вычислить и сохранить длину каждого префикса суффикса L (v) в каждом node дерева. Затем во внутреннем node v в дереве префикс суффикса, закодированный в v, является повторяющейся подпоследовательностью, которая генерирует вашу строку, если N (v) равна общей длине строки, деленной на L (v).
Ответ №3
Хорошо, если каждый символ входной строки является частью повторяющейся подстроки, тогда все, что вам нужно сделать, это сохранить первый символ и сравнить его с остальными символами строки один за другим. Если вы найдете совпадение, строка до тех пор, пока не будет согласована, будет вашей повторяющейся строкой.
Ответ №4
Я тоже искал оптимально-временное решение этой проблемы. Похоже, что принятый ответ tmyklebu, по сути, таков, но я хотел бы предложить некоторое объяснение того, о чем оно на самом деле и некоторые дальнейшие выводы.
Во-первых, этот вопрос, предложенный мной, предлагает, казалось бы, многообещающее, но неправильное решение, с примечаниями о том, почему оно некорректно: корректен ли этот алгоритм для нахождения периода строки?
В общем, проблема “найти период” эквивалентна “найти шаблон внутри себя” (в некотором смысле ” strstr(x+1,x)
“), но без ограничений, соответствующих после ее окончания. Это означает, что вы можете найти период, взяв любой алгоритм сопоставления строк слева направо и применив его к себе, рассматривая частичное совпадение, которое совпадает с концом стога/текста, и требования к времени и пространству так же, как и в любом алгоритме сопоставления строк, который вы используете.
Подход, процитированный в ответе tmyklebu, по существу, применяет этот принцип к сопоставлению строк в упорядоченных алфавитах, также объясняется здесь Другое оптимальное во времени и пространстве решение должно быть возможно с использованием алгоритма GS.
К сожалению, довольно известный и простой двусторонний алгоритм (также описанный здесь) не является решением, поскольку он не слева направо. В частности, улучшение после несоответствия в левом множителе зависит от того, был ли правый фактор совпадением, и невозможность другого совпадения с правильным множителем по модулю периода правильного множителя. При поиске шаблона внутри себя и игнорировании всего, что находится за его концом, мы не можем сделать вывод о том, как скоро может произойти следующее совпадение правильного фактора (часть или весь правильный фактор могли сместиться за конец шаблона), и поэтому сдвиг, который сохраняет линейное время, не может быть сделан.
Конечно, если рабочее пространство доступно, можно использовать ряд других алгоритмов. KMP является линейно-временным с O (n) пространством, и может быть возможно адаптировать его к чему-то еще достаточно эффективному только с логарифмическим пространством.
Связь периода и бордера
Теорема: |
Если у строки длины есть бордер длины , то у нее также имеется период длины . |
Доказательство: |
Пусть дана строка . Напишем формально определение бордера длины строки : Сделаем замену : Получили определение периода длины . Но , значит у строки есть период длины . |
Свойства периода
Теорема (о кратном периоде): |
Если у строки есть период длины , то у нее имеется также период длины , где . |
Доказательство: |
Пусть длина строки равна , сама строка — . Доказательство будем вести индукцией по числу .
Утверждение доказано. |
Перед доказательством следующей теоремы проверим пару интуитивно понятных утверждений.
Лемма (1): |
Пусть строка имеет периоды и , причём . Тогда суффикс и префикс длины имеют период . |
Доказательство: |
Покажем истинность утверждения про префикс; с суффиксом доказательство аналогичное. Требуется показать: Исходя из того, что имеет период , выполнено Также имеет период и из ограничений на верно , поэтому |
Лемма (2): |
Пусть строка имеет период , и существует подстрока такая, что и имеет период , где . Тогда имеет период . |
Доказательство: |
Пусть , где . Требуется показать: . Зафиксируем и . Заметим, что поскольку , отрезок содержит по меньшей мере целых чисел, так что найдутся . С учётом можем написать [1]. Помимо того , а в таком случае верно и . Теперь воспользуемся следующим фактом: если строка имеет период , то (действительно, без ограничения общности можем сказать, что , и исходя из этого выстроить цепочку равенств ). В виду того, что имеет период , имеют место равенства и . Кроме того имеет период , потому верно . Тогда и . |
Теорема (Фин и Вильф): |
Если у строки есть периоды и , где , то также является периодом этой строки. |
Доказательство: |
Обозначим . Доказательство будем вести индукцией по . В случае видим что , что соответствует базе, в то время как при выполнено , так что .
|
См. также
- Основные определения, связанные со строками
Примечания
- ↑ Свойство сравнений (№8)
Источники информации
- Wikipedia — Substring
- Lothaire M. Algebraic Combinatorics on Words — Cambridge University Press, 2002. — с. 272. — ISBN 0-521-81220-8