Skip to main content

Префікс-функція

Дано рядок s[0n1]s[0 \ldots n-1], де nn - довжина рядка. Потрібно обчислити префікс-функцію для нього, тобто масив чисел π[0n1]\pi[0 \ldots n-1], де π[i]\pi[i] визначається наступним чином: це довжина найбільшого власного суфікса підрядка s[0i]s[0 \ldots i], який збігається з її префіксом. Власний суфікс — це такий, який не збігається з усім рядком. Зокрема, значення π[0]\pi[0] дорівнює нулю.

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

π[i]=max(0,maxk=1i { k : s[0k1]=s[ik+1i] }).\pi[i] = \max(0, \max_{k=1 \ldots i} ~ \{ ~ k ~ : ~ s[0 \ldots k-1] = s[i-k+1 \ldots i] ~ \}).

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

  • У рядку "a" немає префікса, який збігається з власним суфіксом.
  • У рядку "ab" немає префікса, який збігається з власним суфіксом.
  • У рядку "abc" немає префікса, який збігається з власним суфіксом.
  • У рядку "abca" префікс довжини 11 збігається з власним суфіксом.
  • У рядку "abcab" префікс довжини 22 збігається з власним суфіксом.
  • У рядку "abcabc" префікс довжини 33 збігається з власним суфіксом.
  • У рядку "abcabcd" немає префікса, який збігається з власним суфіксом.

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

Алгоритм

Безпосередньо дотримуючись визначення, можна написати алгоритм обчислення префікс-функції за допомогою трьох циклів за O(n3)O(n^3), але це є дуже повільно.

Ефективний алгоритм був розроблений Кнутом (Knuth) та Праттом (Pratt), а також незалежно від них Моррісом (Morris) у 1977 році як основна частина алгоритму пошуку підрядка в рядку.

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

Перше важливе зауваження - значення π[i+1]\pi[i+1] не перевищує значення π[i]\pi[i] більше ніж на одиницю для будь-якого ii.

Доведення. В іншому випадку, якщо π[i+1]>π[i]+1\pi[i+1] > \pi[i] + 1, то розглянемо суфікс, який закінчується в позиції i+1i+1 і має довжину π[i+1]\pi[i+1]. Видаливши з нього останній символ, ми отримаємо суфікс, який закінчується в позиції ii і має довжину π[i+1]1\pi[i+1]-1. Це краще, ніж π[i]\pi[i], тому ми дійшли до протиріччя. Ілюстрація цього протиріччя (у цьому прикладі π[i1]\pi[i-1] має бути рівне 3):

s0 s1π[i1]=2 s2 s3π[i]=4  si3 si2 si1π[i1]=2 siπ[i]=4\underbrace{ \overbrace{s_0 \ s_1}^{\pi[i-1]=2} \ s_2 \ s_3}_{\pi[i]=4} \ \ldots\ \underbrace{ s_{i-3}\ \overbrace{s_{i-2}\ s_{i-1}}^{\pi[i-1]=2} \ s_i}_{\pi[i]=4}

На цій схемі верхні фігурні дужки позначають два однакові підрядки довжиною 2, а нижні фігурні дужки - два однакові підрядки довжиною 4.

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

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

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

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

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} \ \ldots\ \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] \ne s[\pi[i]]. Це означає, що нам потрібно спробувати підрядок меншої довжини. З метою оптимізації бажано одразу перейти до такої найбільшої довжини j<π[i]j < \pi[i], що, як і раніше, виконується префікс-властивість в позиції ii, тобто s[0j1]=s[ij+1i]s[0 \ldots j-1] = s[i-j+1 \ldots i].

s0 s1j s2 s3π[i]  si3 si2si1 sijπ[i] si+1\overbrace{\underbrace{s_0 \ s_1}_{j} \ s_2 \ s_3}^{\pi[i]} \ \ldots\ \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, для якого виконується префікс-властивість, і так далі. Може статися, що такі значення 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 \ldots j-1] = s[i-j+1 \ldots i]) потрібно знайти найбільше k<jk < j, для якого, як і раніше, виконується префікс-властивість

s0 s1k s2 s3j  si3 si2si1 sikj si+1\overbrace{\underbrace{s_0 \ s_1}_{k} \ s_2 \ s_3}^{j} \ \ldots\ \overbrace{ s_{i-3}\ s_{i-2} \underbrace{s_{i-1}\ s_{i}}_{k}}^{j} \ s_{i+1}

Значення kk є ніщо інше, як значення префікс-функції π[j1]\pi[j-1], яке ми вже обчислювали раніше (віднімання одиниці зумовлене 0-індексацією рядка). Таким чином, ці довжини kk можна знайти за O(1)O(1) кожну.

Кінцевий алгоритм

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

Наведемо тут схему алгоритму:

  • Значення префікс-функції π[i]\pi[i] будемо визначати по черзі: від i=1i=1 до i=n1i=n-1 (значення π[0]\pi[0] дорівнює нулю).

  • Для підрахунку поточного значення π[i]\pi[i] ми заводимо змінну jj, що позначає довжину поточного розглянутого зразка. Спочатку j=π[i1]j = \pi[i-1].

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

  • Якщо ми дійшли до довжини j=0j=0 і так і не знайшли збігів, то зупиняємо процес перебору зразків, вважаємо π[i]=0\pi[i] = 0 і переходимо до наступного індексу i+1i+1.

Реалізація

Повільне обчислення за O(n3)O(n^3)

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

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

Обчислення алгоритмом Кнута-Морріса-Пратта за O(n)O(n)

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;
}
Зауваження

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

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

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

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

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

Вирішимо спочатку першу задачу. Розглянемо будь-яку позицію ii та значення префікс-функції в ній π[i]\pi[i]. За визначенням, воно означає, що в позиції ii закінчується входження префікса рядка ss довжини π[i]\pi[i], і ніякий більший префікс не може закінчуватись в позиції ii. В той же час, в позиції ii могло закінчуватись входження префіксів менших довжин (і, очевидно, не обов'язково довжини π[i]1\pi[i]-1). Однак, як легко помітити, ми приходимо до того ж питання, на яке ми вже відповіли при розгляді алгоритму обчислення префікс-функції: за заданої довжини jj потрібно знайти найбільший власний суфікс, що збігається з її префіксом. Ми вже встановили, що відповіддю на це питання буде π[j1]\pi[j-1]. Але тоді і в цій задачі, якщо в позиції ii закінчується входження підрядка довжини π[i]\pi[i], що збігається з префіксом, то в ii також закінчується входження підрядка довжини π[π[i]1]\pi[\pi[i]-1], що збігається з префіксом, а для неї застосовні ті ж міркування, тому в ii також закінчується входження довжини π[π[π[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];
}

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

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

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

Дано рядок ss довжини nn. Потрібно порахувати кількість різних підрядків.

Будемо розв'язувати цю задачу ітеративно. Зокрема, навчимося, знаючи поточну кількість різних підрядків, перераховувати цю кількість при додаванні в кінець одного символу.

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

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

Отже, ми отримали, що кількість нових підрядків, які з'являються при додаванні символу cc, дорівнює s.length()+1πmaxs.\operatorname{length}() + 1 - \pi_{\max}.

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

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

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

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

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

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

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

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

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

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\ \ \ \ \Longrightarrow\ \ \ \ s_0=s_1=s_2=s_3

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

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

s0 s1  sn1 #need to savet0 t1  tm1need not to save\underbrace{s_0\ s_1\ \ldots\ s_{n-1}\ \#}_{\rm need\ to\ save} \underbrace{t_0\ t_1\ \ldots\ t_{m-1}}_{\rm need\ not\ to\ save}

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

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

s0 s1  sn1 #π[i1]    s0 s1  sn1 #π[i1]+ti    s0 s1  sn1 #tiπ[i]s_0\ s_1\ \ldots\ s_{n-1}\ \# \underbrace{\ldots}_{\pi[i-1]}\ \ \Longrightarrow\ \ s_0\ s_1\ \ldots\ s_{n-1}\ \# \underbrace{\ldots}_{\pi[i-1]} + t_i\ \ \Longrightarrow\ \ s_0\ s_1\ \ldots\ s_{n-1}\ \# \ldots \underbrace{t_i}_{\pi[i]}

Таким чином, навіть якщо ми ще не маємо рядка tt, ми можемо попередньо побудувати таблицю переходів (стара_π,c)нова_π({\rm стара}\_\pi,c) \rightarrow {\rm нова}\_\pi за допомогою того ж алгоритму обчислення префікс-функції:

вхідний рядок "s"
константа "alphabet" має значення 256, що відповідає потужності алфавіту символів. Зазвичай вона менша

s += '#';

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

Правда, в такому вигляді алгоритм буде працювати за O(nk2)O(nk^2) (kk - потужність алфавіту). Але зауважимо, що замість внутрішнього циклу while\rm while, який поступово скорочує відповідь, ми можемо скористатися вже обчисленою частиною таблиці: переходячи від значення jj до значення π[j1]\pi[j-1], ми фактично говоримо, що перехід зі стану (j,c)(j, c) приведе в той же стан, що і перехід (π[j1],c)(\pi[j-1], c), а для нього відповідь вже точно порахована (тобто π[j1]<j\pi[j-1] < j):

вхідний рядок "s"
константа "alphabet" має значення 256, що відповідає потужності алфавіту символів. Зазвичай це значення менше

s += '#';

int n = (int)s.length();
vector<int> pi = prefix_function(s);
vector<vector<int>> aut(n, vector<int>(alphabet));
for (int i = 0; i < n; ++i) {
for (char c = 0; c < alphabet; ++c) {
if (i > 0 && c != s[i]) {
aut[i][c] = aut[pi[i - 1]][c];
} else {
aut[i][c] = i + (c == s[i]);
}
}
}

В результаті вийшла дуже проста реалізація побудови автомата, яка працює за O(nk)O(n k).

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

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

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

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

g1="a"g_1 = "a"
g2="aba"g_2 = "aba"
g3="abacaba"g_3 = "abacaba"
g4="abacabadabacaba"g_4 = "abacabadabacaba"
\ldots

У таких випадках навіть проста побудова рядка 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.length()s.{\rm length}() під час "годування" рядка gig_i. Зрозуміло, що відповіддю на задачу буде величина 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}. Значить, після "згодовування" першого куска (gi1g_{i-1}) автомат перейде в стан G[i1][j]G[i-1][j], а потім, після "згодовування" символу chari{\rm char}_i, він перейде в стан:

mid=aut[ G[i1][j] ][chari]{\rm mid} = {\rm aut}[\ G[i-1][j]\ ][{\rm char}_i]

Після цього автомату "подаватиметься" останній шматок, тобто gi1g_{i-1}

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

Кількості K[i][j]K[i][j] легко визначаються як сума кількостей по трьох частинах gig_i: рядок gi1g_{i-1}, символ chari{\rm char}_i, і знову рядок gi1g_{i-1}:

K[i][j]=K[i1][j]+(mid==s.length())+K[i1][mid]K[i][j] = K[i-1][j] + ({\rm mid} == s.{\rm length}()) + K[i-1][mid]

Отже, ми вирішили задачу для стрічок Грея. Аналогічно можна розв'язати цілий клас подібних задач. Наприклад, точно таким же методом можна вирішити наступну задачу: дано рядок ss і зразки tit_i, кожен з яких задається наступним чином: це рядок зі звичайних символів, серед яких можуть зустрічатися рекурсивні вставки інших рядків у формі tk[cnt]t_k[\rm cnt], яка означає, що в це місце має бути вставлено cnt\rm cnt екземплярів рядка tkt_k. Один з прикладів такої схеми:

t1="abdeca"t_1 = "abdeca"
t2="abc"+t1[30]+"abd"t_2 = "abc" + t_1[30] + "abd"
t3=t2[50]+t1[100]t_3 = t_2[50] + t_1[100]
t4=t2[10]+t3[100]t_4 = t_2[10] + t_3[100]

Гарантується, що цей опис не містить циклічних залежностей. Обмеження полягають у тому, що якщо явно розкривати рекурсію і знаходити рядки tit_i, то їх довжини можуть досягати порядку 100100100^{100}.

Потрібно знайти кількість входжень рядка ss в кожен з рядків tit_i.

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

Задачі

Список задач, які можна розв'язати, використовуючи префіксну функцію: