Проблема:
Лексикографічно найменш кругла підрядок - це проблема знаходження обертання рядка, що має найнижчий лексикографічний порядок усіх таких обертів. Наприклад, лексикографічно мінімальне обертання "bbaaccaadd" було б "aaccaaddbb".
Рішення:
Алгоритм часу AO (n) був запропонований Жаном П'єром Дювалем (1983).
Враховуючи два індекси i
і j
, алгоритм Дюваля порівнює рядкові відрізки довжини, j - i
починаючи з i
і j
(називається "дуель" ). Якщо index + j - i
більша довжина струни, відрізок формується обертанням навколо.
Наприклад, розглянемо s = "baabbaba", i = 5 і j = 7. Оскільки j - i = 2, перший відрізок, що починається з i = 5, є "ab". Другий відрізок, що починається від j = 7, побудований обертанням навколо, а також є "ab". Якщо рядки лексикографічно рівні, як у наведеному вище прикладі, ми вибираємо той, що починається з i як переможець, а це = 5.
Описаний вище процес повторювався, поки ми не отримаємо єдиного переможця. Якщо вхідний рядок має непарну довжину, останній символ виграє без порівняння в першій ітерації.
Часова складність:
Перша ітерація порівнює n рядків, довжиною 1 (n / 2 порівняння), друга ітерація може порівнювати n / 2 рядка довжиною 2 (n / 2 порівняння) тощо, поки i-та ітерація не порівнює 2 рядки довжина n / 2 (n / 2 порівняння). Оскільки кількість переможців щоразу зменшується вдвічі, то висота дерева рекурсії - log (n), що дає нам алгоритм O (n log (n)). Для малих n це приблизно O (n).
Космічна складність теж є O (n), оскільки в першій ітерації ми повинні зберігати n / 2 переможців, у другій ітерації n / 4 переможців тощо. (Вікіпедія стверджує, що цей алгоритм використовує постійний простір, я не розумію, як).
Ось реалізація Scala; не соромтеся перетворюватися на улюблену мову програмування.
def lexicographicallyMinRotation(s: String): String = {
@tailrec
def duel(winners: Seq[Int]): String = {
if (winners.size == 1) s"${s.slice(winners.head, s.length)}${s.take(winners.head)}"
else {
val newWinners: Seq[Int] = winners
.sliding(2, 2)
.map {
case Seq(x, y) =>
val range = y - x
Seq(x, y)
.map { i =>
val segment = if (s.isDefinedAt(i + range - 1)) s.slice(i, i + range)
else s"${s.slice(i, s.length)}${s.take(s.length - i)}"
(i, segment)
}
.reduce((a, b) => if (a._2 <= b._2) a else b)
._1
case xs => xs.head
}
.toSeq
duel(newWinners)
}
}
duel(s.indices)
}