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

Факторизація Ліндона

Факторизація Ліндона

Спершу означимо поняття факторизації Ліндона.

Рядок називається простим (або словом Ліндона), якщо він строго менший за будь-який зі своїх нетривіальних суфіксів. Приклади простих рядків: aa, bb, abab, aabaab, abbabb, ababbababb, abcdabcd. Можна показати, що рядок є простим тоді й лише тоді, коли він строго менший за всі свої нетривіальні циклічні зсуви.

Далі нехай задано рядок ss. Факторизація Ліндона рядка ss — це розклад s=w1w2wks = w_1 w_2 \dots w_k, де всі рядки wiw_i прості й розташовані в незростаючому порядку w1w2wkw_1 \ge w_2 \ge \dots \ge w_k.

Можна показати, що для будь-якого рядка така факторизація існує і є єдиною.

Коли підходить цей алгоритм?
  • Потрібна факторизація Ліндона рядка або найменший циклічний зсув за O(n)O(n) і O(1)O(1) пам'яті?
  • Задача саме про слова Ліндона / циклічні зсуви, а не про загальний лексикографічний порядок суфіксів? (якщо потрібен порядок усіх суфіксів → Суфіксний масив)
  • Не потрібні запити про входження чи підрядки, лише жадібний прохід по рядку? (якщо потрібні підрядкові запити → Суфіксний автомат)

Алгоритм Дюваля

Алгоритм Дюваля будує факторизацію Ліндона за час O(n)O(n), використовуючи O(1)O(1) додаткової пам'яті.

Спершу введемо ще одне поняття: рядок tt називається передпростим, якщо він має вигляд t=wwwwt = w w \dots w \overline{w}, де ww — простий рядок, а w\overline{w} — префікс ww (можливо, порожній). Простий рядок також є передпростим.

Алгоритм Дюваля жадібний. У будь-який момент його виконання рядок ss фактично поділений на три рядки s=s1s2s3s = s_1 s_2 s_3, де факторизація Ліндона для s1s_1 вже знайдена та зафіксована, рядок s2s_2 є передпростим (і ми знаємо довжину простого рядка в ньому), а s3s_3 зовсім не зачеплений. На кожній ітерації алгоритм Дюваля бере перший символ рядка s3s_3 і намагається дописати його до рядка s2s_2. Якщо s2s_2 перестає бути передпростим, то факторизація Ліндона для деякої частини s2s_2 стає відомою, і ця частина переходить до s1s_1.

Опишемо алгоритм детальніше. Вказівник ii завжди вказуватиме на початок рядка s2s_2. Зовнішній цикл виконуватиметься, доки i<ni < n. Усередині циклу ми використовуємо два додаткові вказівники: jj, який вказує на початок s3s_3, і kk, який вказує на поточний символ, з яким ми зараз порівнюємо. Ми хочемо додати символ s[j]s[j] до рядка s2s_2, що вимагає порівняння із символом s[k]s[k]. Можливі три різні випадки:

  • s[j]=s[k]s[j] = s[k]: якщо це так, то додавання символу s[j]s[j] до s2s_2 не порушує його передпростоти. Тож ми просто збільшуємо вказівники jj і kk.
  • s[j]>s[k]s[j] > s[k]: тут рядок s2+s[j]s_2 + s[j] стає простим. Ми можемо збільшити jj і скинути kk назад на початок s2s_2, щоб наступний символ можна було порівняти з початком простого слова.
  • s[j]<s[k]s[j] < s[k]: рядок s2+s[j]s_2 + s[j] більше не є передпростим. Тому ми розіб'ємо передпростий рядок s2s_2 на його прості рядки та залишок, можливо порожній. Простий рядок матиме довжину jkj - k. На наступній ітерації ми починаємо знову з рештою s2s_2.

Реалізація

Тут ми наводимо реалізацію алгоритму Дюваля, яка повертає шукану факторизацію Ліндона заданого рядка ss.

vector<string> duval(string const& s) {
int n = s.size();
int i = 0;
vector<string> factorization;
while (i < n) {
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k) {
factorization.push_back(s.substr(i, j - k));
i += j - k;
}
}
return factorization;
}

Складність

Оцінімо час роботи цього алгоритму.

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

Тож нас цікавить лише перший внутрішній цикл while. Скільки ітерацій він виконує в найгіршому випадку? Легко бачити, що прості слова, які ми визначаємо на кожній ітерації зовнішнього циклу, довші за залишок, який ми додатково порівнювали. Тому й сума залишків буде меншою за nn, а це означає, що ми виконуємо щонайбільше O(n)O(n) ітерацій першого внутрішнього циклу while. Насправді загальна кількість порівнянь символів не перевищить 4n34n - 3.

Знаходження найменшого циклічного зсуву

Нехай задано рядок ss. Ми будуємо факторизацію Ліндона для рядка s+ss + s (за час O(n)O(n)). Ми шукатимемо в цій факторизації простий рядок, який починається в позиції, меншій за nn (тобто починається в першому входженні ss), і закінчується в позиції, більшій або рівній nn (тобто в другому входженні ss). Стверджується, що позиція початку цього простого рядка буде початком шуканого найменшого циклічного зсуву. Це легко перевірити, скориставшись означенням розкладу Ліндона.

Початок простого блоку можна легко знайти — достатньо запам'ятати вказівник ii на початку кожної ітерації зовнішнього циклу, який позначав початок поточного передпростого рядка.

Отже, отримуємо таку реалізацію:

string min_cyclic_string(string s) {
s += s;
int n = s.size();
int i = 0, ans = 0;
while (i < n / 2) {
ans = i;
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k)
i += j - k;
}
return s.substr(ans, n / 2);
}

Задачі