Самая длинная подстрока, повторяющаяся не менее k раз

Мне дана строка большой длины (скажем, 100 000) и целое число k, и мне нужно вычислить длину наибольшей подстроки, которая повторяется в данной строке не менее k раз.
Я нашел ответы на этот конкретный вопрос. здесь и здесь, но я хотел знать, есть ли другой эффективный способ решить этот вопрос, кроме деревьев суффиксов?


person Shrey Tripathi    schedule 15.04.2020    source источник
comment
Это менее эффективно, но проще для понимания, чем суффиксные деревья: выполните бинарный поиск по длине ответа, вычислите все полиномиальные хэши (cp-algorithms.com/string/string-hashing.html) подстрок такой длины и посмотреть, есть ли хеш, который имеет ›= k вхождений. Вычисление хэшей всех подстрок фиксированной длины занимает O(n), бинарный поиск имеет log n итераций, поэтому общая сложность составляет O(n log n)< /я>.   -  person fas    schedule 15.04.2020
comment
@Fas При выполнении бинарного поиска как вы двигаетесь влево или вправо?   -  person nice_dev    schedule 15.04.2020
comment
@vivek_23 в зависимости от того, есть ли хэш с ›= k вхождениями или нет. Это работает, потому что если есть строка s из с k вхождений, s[:-1] также имеет по крайней мере k вхождений.   -  person fas    schedule 16.04.2020
comment
@fas Но как решить, идти налево или направо? Вполне возможно, что более длинная строка имеет ›= k вхождений?   -  person nice_dev    schedule 16.04.2020
comment
@vivek_23 Если мы проверяем строки длины x и есть хэш с ›= k вхождениями, мы идем влево, чтобы проверить, есть ли строка длиной ‹= x с ›= k вхождений, в противном случае идти вправо. Во время бинарного поиска мы просто пытаемся найти максимальное число x, например, существует строка s с ›= k вхождениями и длиной x, конечно тогда существует строка длины x - 1 (например, s[:-1]), x - 2 (s[:-2]), ..., 1 (например, просто s[0]). Поскольку бинарный поиск находит максимум, невозможно, чтобы строка длиной › x имела ›= k вхождений.   -  person fas    schedule 16.04.2020
comment
@fas Не уверен, правильно ли я понимаю, работает ли abcabcdefgdefgdefgdefg, например, с k = 3, с вашим подходом? ИМО, ответ на это должен быть defg.   -  person nice_dev    schedule 16.04.2020
comment
@ShreyTripathi У вас есть ссылка на проблему?   -  person nice_dev    schedule 16.04.2020
comment
Также нет ответа на ваши предыдущие вопросы!   -  person nice_dev    schedule 16.04.2020
comment
@ vivek_23 нет, не defg в этом примере, этот подход не запрещает пересечение строк, при данном подходе ответ кажется defgdefg. Я не думаю, что суффиксные деревья также могут запрещать пересечение строк.   -  person fas    schedule 16.04.2020
comment
@fas defgdefg встречается только дважды, но k равно 3.   -  person nice_dev    schedule 16.04.2020
comment
@vivek_23 defgdefg приходит 3 раза: abcabc<1>defg<2>defg</1><3>defg</2>defg</3>   -  person fas    schedule 16.04.2020
comment
@fas Ааа, да, они тоже пересекаются. Здесь я начинаю поиск между 1 и 22, середина равна 11. Ни одна подстрока длины 11, кажется, не удовлетворяет. Итак, мы снова идем правильно?   -  person nice_dev    schedule 16.04.2020
comment
@vivek_23 Слева, поиск между 1 и 10, середина 5, defgd существует, например, справа, поиск между 5 и 10, середина 7, defgdef существует, например, справа, поиск между 7 и 10, середина 8, defgdefg существует, справа, поиск между 8 и 10, середина 9, такой строки не существует, слева, поиск между 8 и 8, так что это 8 :)   -  person fas    schedule 16.04.2020
comment
@vivek_23, я очень извиняюсь за поздний ответ! Я просто изучал деревья суффиксов и столкнулся с этой проблемой, и мне стало интересно, есть ли другой алгоритм для решения этой проблемы. У меня нет ссылки на проблему :(   -  person Shrey Tripathi    schedule 16.04.2020
comment
@fas Имеет смысл, но я не думаю, что сбор подстрок длины, скажем, m, является операцией O (1).   -  person nice_dev    schedule 16.04.2020
comment
@ShreyTripathi Хорошо, все в порядке. Я размышляю над предыдущими вопросами, которые вы задавали здесь, на SO. Примите решение, которое подходит лучше всего (это нормально, если вы не примете мое, я просто хочу, чтобы вы ответили)   -  person nice_dev    schedule 16.04.2020
comment
@vivek_23 Мы не собираем подстроки, мы собираем их полиномиальные хэши (что это такое и как это работает можно прочитать здесь cp-algorithms.com/string/string-hashing.html), и это можно сделать в O(n) для всех подстрок фиксированной двоичной длиной поиска m   -  person fas    schedule 16.04.2020
comment
@fas, я не совсем понял твое решение. Я могу выполнить бинарный поиск, только если знаю длину требуемой подстроки. Я этого не знаю (надо это выяснить); или вы подразумеваете, что я начинаю брать длины с 1, 2,....len(string) и создаю отдельную хэш-таблицу для каждой из них? Действительно извините за поздний ответ   -  person Shrey Tripathi    schedule 16.04.2020
comment
@fas хорошая информация, я посмотрю на это.   -  person nice_dev    schedule 16.04.2020
comment
@ShreyTripathi мы выполняем двоичный поиск, чтобы найти максимальную длину (см. пример для строки abcabcdefgdefgdefgdefg здесь stackoverflow.com/questions/61235589/ и в других комментариях выше), мы создаем отдельную хеш-таблицу во время двоичного поиска для каждого значения mid (которое это длина, которую мы проверяем, можно ли найти в ней k подстрок такой длины)   -  person fas    schedule 17.04.2020
comment
@vivek_23 Я написал длинный ответ, чтобы подвести итог нашей дискуссии, который, я надеюсь, все объясняет. stackoverflow.com/a/61261931/3365922   -  person fas    schedule 17.04.2020


Ответы (1)


В комментариях была большая дискуссия, думаю лучше написать ответ, чтобы подвести итоги. TL;DR Самая длинная подстрока, повторяющаяся не менее k раз

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

<сильный>1. Строковый полиномиальный хеш

Читайте об этом здесь https://cp-algorithms.com/string/string-hashing. HTML. Ниже приводится краткое описание этой техники.

Допустим, у нас есть строка s, целые числа p и mod. Теперь мы можем определить хэш-функцию:

hash(s) = (ord(s[0])*p^(len(s)-1) + ord(s[1])*p^(len(s)-2) + ... + ord(s[len(s)-1])*p^0) % mod 

где ord — функция, возвращающая целое число по символу (допустим, это ASCII-код символа). Полиномиальный хэш можно легко вычислить для каждого префикса строки в O(len(s)):

# h[i] is a hash of prefix of length i.
# For example s = "abacaba",
# h[0] = hash("") = 0
# h[1] = hash("a")
# h[2] = hash("ab")
# ...
# h[7] = hash("abacaba")

h[0] = 0
for i in 1..n:
    h[i] = (h[i-1] * p + ord(s[i-1])) % mod

Также предварительно вычислим p^0 % mod, p^1 % mod, ..., p^len(s) % mod в массиве pow:

# pow[i] is (p^i) % mod
pow[0] = 1
for i in 1..n:
    pow[i] = (pow[i-1] * p) % mod

Используя массивы h и pow, мы можем легко вычислить хэш любой подстроки строки s:

# get_substring_hash returns hash(s[l] + s[l+1] + ... + s[r-1]).
def get_substring_hash(s, l, r):
    value = h[r] - h[l]*pow[r-l]    # line a
    return (value%mod + mod) % mod  # line b

Давайте разберемся, почему код выше работает.

h[r] = (ord(s[r-1])*p^0 + ord(s[r-2])*p^1 + ... + ord(s[l-1])*p^(r-l) + ord(s[l-2])*p^(r-l+1) + ...) % mod
h[l] = (                                          ord(s[l-1])*p^0     + ord(s[l-2])*p^1       + ...) % mod
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Как видите, нам нужна только ^^^-часть из h[r], поэтому мы должны избавиться от ~~~-части. ~~~-часть в h[r] p^(r-l) раз больше, чем в h[l], и это объясняет строку a.

Строка b — своего рода магия при работе с % mod, value после строки a может быть отрицательной, поэтому value%mod + mod определенно дает положительное значение. В то же время, если value было положительным после строки a, value%mod + mod больше, чем mod, поэтому (value%mod + mod) % mod обязательно вернет значение в диапазоне 0, 1, ..., mod-1 .

Наконец, mod — это большое простое число, такое как 10^9+7, а value — это небольшое число, но оно больше любого возможного ASCII-кода, такого как 239 (читайте в статье, почему так).

Некоторые примечания:

  • Хеши могут конфликтовать, потому что у нас есть только mod возможных значений хеша, а количество строк бесконечно. Как с этим бороться читайте в статье.
  • Выполнение таких операций, как h[r] - h[l]*pow[r-l], может потребовать использования 64-битных типов целых чисел.

<сильный>2. Бинарный поиск

Просто прочитайте об этом в Википедии, там нет ничего конкретного https://en.wikipedia.org/wiki/Binary_search_algorithm< /а>.

<сильный>3. Самая длинная подстрока, повторяющаяся как минимум k раз

Допустим, мы предварительно вычислили массивы h и pow. Выполним бинарный поиск, чтобы найти максимальную длину строки ans, такую, чтобы в заданной строке s было k или более таких подстрок.

Почему здесь работает бинарный поиск? Потому что если есть какая-то длина x, например, k или более равных подстрок в s длины x, то в s длины x-1 определенно есть k или более равных подстрок (просто удалите последнюю букву из наших совпадений).

Как будет работать бинарный поиск? Допустим, мы сейчас проверяем, есть ли k или более одинаковых подстрок длины mid. Мы будем вычислять все хэши длины mid (используя get_substring_hash) и примем решение об изменении границ бинарного поиска, есть k равных хэшей или нет.

Например: s = "abcabcdefgdefgdefgdefg", k = 3. Ответ: "defgdefg":

abcabcdefgdefgdefgdefg
      ^^^^^^^^          occurence 1
          ^^^^^^^^      occurence 2
              ^^^^^^^^  occurence 3

Итерации бинарного поиска:

l =  1, r = 22, mid = 11. No substring of length 11 satisfy.
l =  1, r = 10, mid =  5. There should be hash("defgd")    be seen 3 times.
l =  5, r = 10, mid =  7. There should be hash("defgdef")  be seen 3 times.
l =  7, r = 10, mid =  8. There should be hash("defgdefg") be seen 3 times.
l =  8, r = 10, mid =  9. No substring of length 9  satisfy.
l =  8, r =  8.           That means answer is 8.

Как видите, сложность составляет O(n log n): round(log n) итераций бинарного поиска и O(n) сложность на итерация, если вы используете что-то вроде std::unordered_map, чтобы проверить, есть ли хэш с >= k вхождениями или нет.

Очень надеюсь, что теперь все ясно.

person fas    schedule 16.04.2020
comment
Большое спасибо за ваш ответ. Я понял алгоритм. Только одно, что здесь "p"? Это может быть любое число, верно? - person Shrey Tripathi; 17.04.2020
comment
Это определенно может быть простое число, большее любого числа, возвращаемого вашей функцией ord (больше любого ASCII-кода). Я не могу точно сказать, что p может быть не просто, кажется, это тоже сработает. - person fas; 17.04.2020