Перейти до основного вмісту

Префікс-функція. Алгоритм Кнута–Морріса–Пратта

Означення префікс-функції

Нам дано рядок ss довжини nn. Префікс-функція для цього рядка означається як масив π\pi довжини nn, де π[i]\pi[i] — це довжина найдовшого власного префікса підрядка s[0i]s[0 \dots i], який водночас є суфіксом цього підрядка. Власний префікс рядка — це префікс, який не дорівнює самому рядку. За означенням π[0]=0\pi[0] = 0.

Математично означення префікс-функції можна записати так:

π[i]=maxk=0i{k:s[0k1]=s[i(k1)i]}\pi[i] = \max_ {k = 0 \dots i} \{k : s[0 \dots k-1] = s[i-(k-1) \dots i] \}

Наприклад, префікс-функція рядка "abcabcd" дорівнює [0,0,0,1,2,3,0][0, 0, 0, 1, 2, 3, 0], а префікс-функція рядка "aabaaab" дорівнює [0,1,0,1,2,2,3][0, 1, 0, 1, 2, 2, 3].

Коли підходить цей алгоритм?
  • Шукаєте один взірець у тексті (або працюєте з періодами/бордерами одного рядка)? (якщо взірців багато одночасно → Ахо-Корасік)
  • Потрібен точний збіг підрядка, а не порівняння довільних підрядків за O(1)O(1)? (якщо потрібні саме порівняння будь-яких підрядків → Хешування рядків)
  • Достатньо лінійних структур, без багатьох запитів про суфікси/підрядки? (якщо потрібні лексикографічні запити на суфіксах → Суфіксний масив)

Тривіальний алгоритм

Алгоритм, який точно слідує означенню префікс-функції, такий:

vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n);
for (int i = 0; i < n; i++)
for (int k = 0; k <= i; k++)
if (s.substr(0, k) == s.substr(i-k+1, k))
pi[i] = k;
return pi;
}

Легко бачити, що його складність становить O(n3)O(n^3), що залишає простір для покращення.

Ефективний алгоритм

Цей алгоритм запропонували Кнут і Пратт та незалежно від них Морріс у 1977 році. Його використовували як основну функцію алгоритму пошуку підрядка.

Перша оптимізація

Перше важливе спостереження полягає в тому, що значення префікс-функції можуть зростати щонайбільше на одиницю.

Справді, інакше, якби π[i+1]>π[i]+1\pi[i + 1] \gt \pi[i] + 1, то ми могли б узяти цей суфікс, що закінчується в позиції i+1i + 1, довжини π[i+1]\pi[i + 1] і прибрати з нього останній символ. Ми отримали б суфікс, що закінчується в позиції ii, довжини π[i+1]1\pi[i + 1] - 1, що краще за π[i]\pi[i], тобто маємо суперечність.

Наступна ілюстрація показує цю суперечність. Найдовший власний суфікс у позиції ii, який водночас є префіксом, має довжину 22, а в позиції i+1i+1 — довжину 44. Тому рядок s0 s1 s2 s3s_0 ~ s_1 ~ s_2 ~ s_3 дорівнює рядку si2 si1 si si+1s_{i-2} ~ s_{i-1} ~ s_i ~ s_{i+1}, а це означає, що й рядки s0 s1 s2s_0 ~ s_1 ~ s_2 та si2 si1 sis_{i-2} ~ s_{i-1} ~ s_i рівні, отже π[i]\pi[i] має дорівнювати 33.

s0 s1π[i]=2 s2 s3π[i+1]=4  si2 si1 siπ[i]=2 si+1π[i+1]=4\underbrace{\overbrace{s_0 ~ s_1}^{\pi[i] = 2} ~ s_2 ~ s_3}_{\pi[i+1] = 4} ~ \dots ~ \underbrace{s_{i-2} ~ \overbrace{s_{i-1} ~ s_{i}}^{\pi[i] = 2} ~ s_{i+1}}_{\pi[i+1] = 4}

Отже, переходячи до наступної позиції, значення префікс-функції може або зрости на одиницю, або лишитися тим самим, або зменшитися на якусь величину. Цей факт уже дозволяє нам знизити складність алгоритму до O(n2)O(n^2), бо за один крок префікс-функція може зрости щонайбільше на одиницю. Загалом функція може зрости щонайбільше nn кроків, а отже й зменшитися загалом теж щонайбільше nn кроків. Це означає, що нам потрібно виконати лише O(n)O(n) порівнянь рядків, і ми досягаємо складності O(n2)O(n^2).

Друга оптимізація

Підемо далі — ми хочемо позбутися порівнянь рядків. Щоб цього досягти, нам треба використати всю інформацію, обчислену на попередніх кроках.

Отже, обчислимо значення префікс-функції π\pi для i+1i + 1. Якщо s[i+1]=s[π[i]]s[i+1] = s[\pi[i]], то ми можемо з упевненістю сказати, що π[i+1]=π[i]+1\pi[i+1] = \pi[i] + 1, оскільки ми вже знаємо, що суфікс у позиції ii довжини π[i]\pi[i] дорівнює префіксу довжини π[i]\pi[i]. Це знову проілюстровано на прикладі.

s0 s1 s2π[i] s3s3=si+1π[i+1]=π[i]+1  si2 si1 siπ[i] si+1s3=si+1π[i+1]=π[i]+1\underbrace{\overbrace{s_0 ~ s_1 ~ s_2}^{\pi[i]} ~ \overbrace{s_3}^{s_3 = s_{i+1}}}_{\pi[i+1] = \pi[i] + 1} ~ \dots ~ \underbrace{\overbrace{s_{i-2} ~ s_{i-1} ~ s_{i}}^{\pi[i]} ~ \overbrace{s_{i+1}}^{s_3 = s_{i + 1}}}_{\pi[i+1] = \pi[i] + 1}

Якщо ж це не так, s[i+1]s[π[i]]s[i+1] \neq s[\pi[i]], то нам потрібно спробувати коротший рядок. Щоб пришвидшити справу, ми хотіли б одразу перейти до найбільшої довжини j<π[i]j \lt \pi[i] такої, що в позиції ii виконується префіксна властивість, тобто s[0j1]=s[ij+1i]s[0 \dots j-1] = s[i-j+1 \dots i]:

s0 s1j s2 s3π[i]  si3 si2 si1 sijπ[i] si+1\overbrace{\underbrace{s_0 ~ s_1}_j ~ s_2 ~ s_3}^{\pi[i]} ~ \dots ~ \overbrace{s_{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}_j}^{\pi[i]} ~ s_{i+1}

Справді, якщо ми знайдемо таку довжину jj, то нам знову потрібно лише порівняти символи s[i+1]s[i+1] та s[j]s[j]. Якщо вони рівні, то ми можемо присвоїти π[i+1]=j+1\pi[i+1] = j + 1. Інакше нам потрібно буде знайти найбільше значення, менше за jj, для якого виконується префіксна властивість, і так далі. Може статися, що це триватиме до j=0j = 0. Якщо тоді s[i+1]=s[0]s[i+1] = s[0], ми присвоюємо π[i+1]=1\pi[i+1] = 1, а інакше π[i+1]=0\pi[i+1] = 0.

Отже, у нас уже є загальна схема алгоритму. Лишається єдине запитання — як ефективно знаходити довжини для jj. Підсумуймо: для поточної довжини jj у позиції ii, для якої виконується префіксна властивість, тобто s[0j1]=s[ij+1i]s[0 \dots j-1] = s[i-j+1 \dots i], ми хочемо знайти найбільше k<jk \lt j, для якого виконується префіксна властивість.

s0 s1k s2 s3j  si3 si2 si1 sikj si+1\overbrace{\underbrace{s_0 ~ s_1}_k ~ s_2 ~ s_3}^j ~ \dots ~ \overbrace{s_{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}_k}^j ~s_{i+1}

Ілюстрація показує, що це має бути значення π[j1]\pi[j-1], яке ми вже обчислили раніше.

Остаточний алгоритм

Отже, ми нарешті можемо побудувати алгоритм, який не виконує жодних порівнянь рядків і виконує лише O(n)O(n) дій.

Ось остаточна процедура:

  • Ми обчислюємо значення префікс-функції π[i]\pi[i] у циклі, ітеруючи від i=1i = 1 до i=n1i = n-1 (значенню π[0]\pi[0] просто присвоюємо 00).
  • Щоб обчислити поточне значення π[i]\pi[i], ми задаємо змінну jj, що позначає довжину найкращого суфікса для i1i-1. Спочатку j=π[i1]j = \pi[i-1].
  • Перевіряємо, чи суфікс довжини j+1j+1 є також і префіксом, порівнюючи s[j]s[j] і s[i]s[i]. Якщо вони рівні, то присвоюємо π[i]=j+1\pi[i] = j + 1, інакше зменшуємо jj до π[j1]\pi[j-1] і повторюємо цей крок.
  • Якщо ми досягли довжини j=0j = 0 і досі не маємо збігу, то присвоюємо π[i]=0\pi[i] = 0 і переходимо до наступного індексу i+1i + 1.

Реалізація

Реалізація виявляється напрочуд короткою та виразною.

vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n);
for (int i = 1; i < n; i++) {
int j = pi[i-1];
while (j > 0 && s[i] != s[j])
j = pi[j-1];
if (s[i] == s[j])
j++;
pi[i] = j;
}
return pi;
}

Це онлайн-алгоритм, тобто він обробляє дані в міру їх надходження — наприклад, можна читати символи рядка по одному й одразу їх обробляти, знаходячи значення префікс-функції для кожного наступного символу. Алгоритм усе ще потребує зберігання самого рядка та раніше обчислених значень префікс-функції, але якщо ми наперед знаємо максимальне значення MM, яке префікс-функція може набути на рядку, то ми можемо зберігати лише M+1M+1 перших символів рядка та таку саму кількість значень префікс-функції.

Застосування

Пошук підрядка в рядку. Алгоритм Кнута–Морріса–Пратта

Це задача є класичним застосуванням префікс-функції.

Дано текст tt і рядок ss, ми хочемо знайти та вивести позиції всіх входжень рядка ss у текст tt.

Для зручності позначимо через nn довжину рядка ss, а через mm — довжину тексту tt.

Ми утворюємо рядок s+#+ts + \# + t, де #\# — це роздільник, який не зустрічається ані в ss, ані в tt. Обчислимо префікс-функцію для цього рядка. Тепер подумаймо про значення префікс-функції, крім перших n+1n + 1 елементів (які належать рядку ss і роздільнику). За означенням значення π[i]\pi[i] показує найбільшу довжину підрядка, що закінчується в позиції ii, який збігається з префіксом. Але в нашому випадку це не що інше, як найбільший блок, що збігається з ss і закінчується в позиції ii. Ця довжина не може бути більшою за nn через роздільник. Але якщо досягається рівність π[i]=n\pi[i] = n, то це означає, що рядок ss повністю з’являється в цій позиції, тобто закінчується в позиції ii. Тільки не забуваймо, що позиції індексуються в рядку s+#+ts + \# + t.

Отже, якщо в якійсь позиції ii ми маємо π[i]=n\pi[i] = n, то в позиції i(n+1)n+1=i2ni - (n + 1) - n + 1 = i - 2n у рядку tt з’являється рядок ss.

Як уже згадувалося в описі обчислення префікс-функції, якщо ми знаємо, що значення префікса ніколи не перевищують певного значення, то нам не потрібно зберігати весь рядок і всю функцію, а лише її початок. У нашому випадку це означає, що нам потрібно зберігати лише рядок s+#s + \# і значення префікс-функції для нього. Ми можемо читати рядок tt по одному символу за раз і обчислювати поточне значення префікс-функції.

Отже, алгоритм Кнута–Морріса–Пратта розв’язує задачу за час O(n+m)O(n + m) і пам’ять O(n)O(n).

Підрахунок кількості входжень кожного префікса

Тут ми обговорюємо одразу дві задачі. Дано рядок ss довжини nn. У першому варіанті задачі ми хочемо підрахувати кількість появ кожного префікса s[0i]s[0 \dots i] у тому самому рядку. У другому варіанті задачі дано інший рядок tt, і ми хочемо підрахувати кількість появ кожного префікса s[0i]s[0 \dots i] у tt.

Спочатку розв’яжемо першу задачу. Розгляньмо значення префікс-функції π[i]\pi[i] у позиції ii. За означенням це означає, що префікс довжини π[i]\pi[i] рядка ss зустрічається й закінчується в позиції ii, і немає довшого префікса, який задовольняв би це означення. Водночас коротші префікси можуть закінчуватися в цій позиції. Неважко бачити, що ми маємо те саме запитання, на яке ми вже відповіли, коли обчислювали саму префікс-функцію: дано префікс довжини jj, який є суфіксом, що закінчується в позиції ii, — який наступний менший префікс <j\lt j, що теж є суфіксом, який закінчується в позиції ii. Отже, у позиції ii закінчується префікс довжини π[i]\pi[i], префікс довжини π[π[i]1]\pi[\pi[i] - 1], префікс π[π[π[i]1]1]\pi[\pi[\pi[i] - 1] - 1], і так далі, доки індекс не стане нулем. Отже, ми можемо обчислити відповідь у такий спосіб.

vector<int> ans(n + 1);
for (int i = 0; i < n; i++)
ans[pi[i]]++;
for (int i = n-1; i > 0; i--)
ans[pi[i-1]] += ans[i];
for (int i = 0; i <= n; i++)
ans[i]++;

Тут для кожного значення префікс-функції ми спочатку рахуємо, скільки разів воно зустрічається в масиві π\pi, а потім обчислюємо остаточні відповіді: якщо ми знаємо, що префікс довжини ii з’являється рівно ans[i]\text{ans}[i] разів, то це число треба додати до кількості входжень його найдовшого суфікса, який водночас є префіксом. Наприкінці нам потрібно додати 11 до кожного результату, оскільки нам потрібно врахувати й самі вихідні префікси.

Тепер розгляньмо другу задачу. Ми застосовуємо трюк з алгоритму Кнута–Морріса–Пратта: ми утворюємо рядок s+#+ts + \# + t і обчислюємо його префікс-функцію. Єдина відмінність від першої задачі полягає в тому, що нас цікавлять лише значення префікса, які стосуються рядка tt, тобто π[i]\pi[i] для in+1i \ge n + 1. З цими значеннями ми можемо виконати точно такі самі обчислення, як у першій задачі.

Кількість різних підрядків у рядку

Дано рядок ss довжини nn. Ми хочемо обчислити кількість різних підрядків, що в ньому зустрічаються.

Ми розв’язуватимемо цю задачу ітеративно. А саме, ми навчимося, знаючи поточну кількість різних підрядків, перераховувати цю кількість, додаючи символ у кінець.

Отже, нехай kk — це поточна кількість різних підрядків у ss, і ми додаємо символ cc у кінець ss. Очевидно, що з’являться деякі нові підрядки, які закінчуються на cc. Ми хочемо підрахувати ці нові підрядки, яких не було раніше.

Ми беремо рядок t=s+ct = s + c і обертаємо його. Тепер задача перетворюється на обчислення того, скільки є префіксів, які більше ніде не зустрічаються. Якщо ми обчислимо максимальне значення префікс-функції πmax\pi_{\text{max}} оберненого рядка tt, то найдовший префікс, який зустрічається в ss, має довжину πmax\pi_{\text{max}}. Зрозуміло, що в ньому також зустрічаються всі префікси меншої довжини.

Тому кількість нових підрядків, що з’являються, коли ми додаємо новий символ cc, дорівнює s+1πmax|s| + 1 - \pi_{\text{max}}.

Отже, для кожного доданого символу ми можемо обчислити кількість нових підрядків за час O(n)O(n), що дає часову складність O(n2)O(n^2) загалом.

Варто зауважити, що ми також можемо обчислити кількість різних підрядків, додаючи символи на початок, або видаляючи символи з початку чи з кінця.

Стиснення рядка

Дано рядок ss довжини nn. Ми хочемо знайти найкоротше «стиснуте» подання рядка, тобто ми хочемо знайти рядок tt найменшої довжини такий, що ss можна подати як конкатенацію однієї або кількох копій tt.

Зрозуміло, що нам потрібно знайти лише довжину tt. Знаючи довжину, відповіддю до задачі буде префікс ss цієї довжини.

Обчислимо префікс-функцію для ss. Використовуючи її останнє значення, ми визначаємо величину k=nπ[n1]k = n - \pi[n - 1]. Ми покажемо, що якщо kk ділить nn, то kk буде відповіддю, інакше ефективного стиснення не існує, і відповідь — це nn.

Нехай nn ділиться на kk. Тоді рядок можна розбити на блоки довжини kk. За означенням префікс-функції префікс довжини nkn - k дорівнюватиме своєму суфіксу. Але це означає, що останній блок дорівнює блоку перед ним. А блок перед ним має дорівнювати блоку перед ним. І так далі. У результаті виявляється, що всі блоки рівні, отже, ми можемо стиснути рядок ss до довжини kk.

Звісно, нам ще потрібно показати, що це справді оптимум. Справді, якби існувало стиснення менше за kk, то префікс-функція в кінці була б більшою за nkn - k. Тому kk справді є відповіддю.

Тепер припустімо, що nn не ділиться на kk. Ми покажемо, що це означає, що довжина відповіді дорівнює nn. Доведемо це від супротивного. Припустимо, що відповідь існує, і стиснення має довжину pp (pp ділить nn). Тоді останнє значення префікс-функції має бути більшим за npn - p, тобто суфікс частково накриватиме перший блок. Тепер розгляньмо другий блок рядка. Оскільки префікс дорівнює суфіксу, і обидва — і префікс, і суфікс — накривають цей блок, а їхнє зміщення одне відносно одного kk не ділить довжину блоку pp (інакше kk ділило б nn), то всі символи блоку мають бути однаковими. Але тоді рядок складається лише з одного символу, повтореного знову і знову, отже, ми можемо стиснути його до рядка розміру 11, що дає k=1k = 1, і kk ділить nn. Суперечність.

s0 s1 s2 s3p s4 s5 s6 s7p\overbrace{s_0 ~ s_1 ~ s_2 ~ s_3}^p ~ \overbrace{s_4 ~ s_5 ~ s_6 ~ s_7}^p s0 s1 s2 s3 s4 s5 s6p s7π[7]=5s_0 ~ s_1 ~ s_2 ~ \underbrace{\overbrace{s_3 ~ s_4 ~ s_5 ~ s_6}^p ~ s_7}_{\pi[7] = 5} s4=s3, s5=s4, s6=s5, s7=s6  s0=s1=s2=s3s_4 = s_3, ~ s_5 = s_4, ~ s_6 = s_5, ~ s_7 = s_6 ~ \Rightarrow ~ s_0 = s_1 = s_2 = s_3

Побудова автомата за префікс-функцією

Повернімося до конкатенації двох рядків через роздільник, тобто для рядків ss і tt ми обчислюємо префікс-функцію рядка s+#+ts + \# + t. Очевидно, оскільки #\# — це роздільник, значення префікс-функції ніколи не перевищить s|s|. Звідси випливає, що достатньо зберігати лише рядок s+#s + \# і значення префікс-функції для нього, а префікс-функцію для всіх наступних символів ми можемо обчислювати на льоту:

s0 s1  sn1 #need to store t0 t1  tm1do not need to store\underbrace{s_0 ~ s_1 ~ \dots ~ s_{n-1} ~ \#}_{\text{need to store}} ~ \underbrace{t_0 ~ t_1 ~ \dots ~ t_{m-1}}_{\text{do not need to store}}

Справді, у такій ситуації, знаючи наступний символ ctc \in t і значення префікс-функції попередньої позиції, ми маємо достатньо інформації, щоб обчислити наступне значення префікс-функції, не використовуючи жодних попередніх символів рядка tt і значень префікс-функції в них.

Іншими словами, ми можемо побудувати автомат (скінченний автомат): станом у ньому є поточне значення префікс-функції, а перехід з одного стану в інший виконуватиметься через наступний символ.

Отже, навіть не маючи рядка tt, ми можемо побудувати таку таблицю переходів (oldπ,c)newπ(\text{old}_\pi, c) \rightarrow \text{new}_\pi, використовуючи той самий алгоритм, що й для обчислення таблиці переходів:

void compute_automaton(string s, vector<vector<int>>& aut) {
s += '#';
int n = s.size();
vector<int> pi = prefix_function(s);
aut.assign(n, vector<int>(26));
for (int i = 0; i < n; i++) {
for (int c = 0; c < 26; c++) {
int j = i;
while (j > 0 && 'a' + c != s[j])
j = pi[j-1];
if ('a' + c == s[j])
j++;
aut[i][c] = j;
}
}
}

Однак у такій формі алгоритм працює за час O(n226)O(n^2 26) для малих літер алфавіту. Зауважимо, що ми можемо застосувати динамічне програмування й використати вже обчислені частини таблиці. Щоразу, коли ми переходимо від значення jj до значення π[j1]\pi[j-1], ми насправді маємо на увазі, що перехід (j,c)(j, c) веде до того самого стану, що й перехід (π[j1],c)(\pi[j-1], c), а ця відповідь уже точно обчислена.

void compute_automaton(string s, vector<vector<int>>& aut) {
s += '#';
int n = s.size();
vector<int> pi = prefix_function(s);
aut.assign(n, vector<int>(26));
for (int i = 0; i < n; i++) {
for (int c = 0; c < 26; c++) {
if (i > 0 && 'a' + c != s[i])
aut[i][c] = aut[pi[i-1]][c];
else
aut[i][c] = i + ('a' + c == s[i]);
}
}
}

У результаті ми будуємо автомат за час O(26n)O(26 n).

Коли такий автомат корисний? Для початку згадаймо, що ми використовуємо префікс-функцію для рядка s+#+ts + \# + t і її значення переважно з єдиною метою: знайти всі входження рядка ss у рядок tt.

Тому найочевидніша користь цього автомата — це пришвидшення обчислення префікс-функції для рядка s+#+ts + \# + t. Побудувавши автомат для s+#s + \#, нам більше не потрібно зберігати рядок ss чи значення префікс-функції в ньому. Усі переходи вже обчислені в таблиці.

Але є й друге, менш очевидне застосування. Ми можемо використовувати автомат, коли рядок tt — це гігантський рядок, побудований за певними правилами. Це можуть бути, наприклад, рядки Грея або рядок, утворений рекурсивною комбінацією кількох коротких рядків із вхідних даних.

Для повноти ми розв’яжемо таку задачу: дано число k105k \le 10^5 і рядок ss довжини 105\le 10^5. Нам потрібно обчислити кількість входжень ss у kk-й рядок Грея. Нагадаймо, що рядки Грея означаються так:

g1="a"g2="aba"g3="abacaba"g4="abacabadabacaba"\begin{align} g_1 &= \text{"a"}\\ g_2 &= \text{"aba"}\\ g_3 &= \text{"abacaba"}\\ g_4 &= \text{"abacabadabacaba"} \end{align}

У таких випадках навіть побудувати рядок tt буде неможливо через його астрономічну довжину. kk-й рядок Грея має довжину 2k12^k-1 символів. Однак ми можемо ефективно обчислити значення префікс-функції в кінці рядка, знаючи лише значення префікс-функції на початку.

Окрім самого автомата, ми також обчислюємо значення G[i][j]G[i][j] — значення автомата після обробки рядка gig_i, починаючи зі стану jj. А додатково ми обчислюємо значення K[i][j]K[i][j] — кількість входжень ss у gig_i під час обробки gig_i, починаючи зі стану jj. Власне, K[i][j]K[i][j] — це кількість разів, коли префікс-функція набула значення s|s| під час виконання операцій. Відповіддю до задачі тоді буде K[k][0]K[k][0].

Як ми можемо обчислити ці значення? Спочатку базові значення такі: G[0][j]=jG[0][j] = j і K[0][j]=0K[0][j] = 0. А всі наступні значення можна обчислити з попередніх значень, використовуючи автомат. Щоб обчислити значення для деякого ii, згадаймо, що рядок gig_i складається з gi1g_{i-1}, ii-го символу алфавіту та gi1g_{i-1}. Отже, автомат перейде у стан:

mid=aut[G[i1][j]][i]\text{mid} = \text{aut}[G[i-1][j]][i] G[i][j]=G[i1][mid]G[i][j] = G[i-1][\text{mid}]

Значення для K[i][j]K[i][j] також легко підрахувати.

K[i][j]=K[i1][j]+(mid==s)+K[i1][mid]K[i][j] = K[i-1][j] + (\text{mid} == |s|) + K[i-1][\text{mid}]

Отже, ми можемо розв’язати задачу для рядків Грея, а так само й величезну кількість інших подібних задач. Наприклад, точно той самий метод також розв’язує таку задачу: нам дано рядок ss і деякі взірці tit_i, кожен з яких задається так: це рядок зі звичайних символів, і там можуть бути деякі рекурсивні вставки попередніх рядків виду tkcntt_k^{\text{cnt}}, що означає, що в цьому місці ми маємо вставити рядок tkt_k cnt\text{cnt} разів. Приклад таких взірців:

t1="abdeca"t2="abc"+t130+"abd"t3=t250+t1100t4=t210+t3100\begin{align} t_1 &= \text{"abdeca"}\\ t_2 &= \text{"abc"} + t_1^{30} + \text{"abd"}\\ t_3 &= t_2^{50} + t_1^{100}\\ t_4 &= t_2^{10} + t_3^{100} \end{align}

Рекурсивні підстановки роздувають рядок так, що їхні довжини можуть сягати порядку 100100100^{100}.

Нам потрібно знайти кількість разів, коли рядок ss з’являється в кожному з рядків.

Задачу можна розв’язати тим самим способом, побудувавши автомат префікс-функції, а потім ми обчислюємо переходи для кожного взірця, використовуючи попередні результати.

Задачі для практики

Відеоматеріали