Декомпозиция Линдона — различия между версиями
Shersh (обсуждение | вклад) (→Реализация) |
(→Алгоритм Дюваля) |
||
| Строка 84: | Строка 84: | ||
Во время работы алгоритма строка <tex>s</tex> разделена на три строки <tex>s = s_1s_2s_3</tex>, где в строке <tex>s_1</tex> декомпозиция Линдона уже найдена и <tex>s_1</tex> уже больше не используется алгоритмом; строка <tex>s_2</tex> {{---}} это предпростая строка (причём длину простых строк внутри неё мы также запоминаем); строка <tex>s_3</tex> {{---}} это ещё не обработанная часть строки <tex>s</tex>. Алгоритм Дюваля берёт первый символ строки <tex>s_3</tex> и пытается дописать его к строке <tex>s_2</tex>. При этом, возможно, для какого-то префикса строки <tex>s_2</tex> декомпозиция Линдона становится известной, и эта часть переходит к строке <tex>s_1</tex>. | Во время работы алгоритма строка <tex>s</tex> разделена на три строки <tex>s = s_1s_2s_3</tex>, где в строке <tex>s_1</tex> декомпозиция Линдона уже найдена и <tex>s_1</tex> уже больше не используется алгоритмом; строка <tex>s_2</tex> {{---}} это предпростая строка (причём длину простых строк внутри неё мы также запоминаем); строка <tex>s_3</tex> {{---}} это ещё не обработанная часть строки <tex>s</tex>. Алгоритм Дюваля берёт первый символ строки <tex>s_3</tex> и пытается дописать его к строке <tex>s_2</tex>. При этом, возможно, для какого-то префикса строки <tex>s_2</tex> декомпозиция Линдона становится известной, и эта часть переходит к строке <tex>s_1</tex>. | ||
| − | Будем поддерживаться указатель <tex>i</tex> на начало строки <tex>s_2</tex>. Внешний цикл алгоритма будет выполняться, пока <tex>i < n</tex>, то есть пока вся строка <tex>s</tex> не перейдёт в строку <tex>s_1</tex>. Внутри этого цикла создаются два указателя: указатель <tex>j</tex> на начало строки <tex>s_3</tex> и указатель <tex>k</tex> на текущий символ в строке <tex>s_2</tex>, с которым будет производиться сравнение. Затем будем в цикле пытаться добавить символ <tex>s[j]</tex> к строке <tex>s_2</tex>, для чего необходимо произвести сравнение с символом <tex>s[k]</tex>. | + | Будем поддерживаться указатель <tex>i</tex> на начало строки <tex>s_2</tex>. Внешний цикл алгоритма будет выполняться, пока <tex>i < n</tex>, то есть пока вся строка <tex>s</tex> не перейдёт в строку <tex>s_1</tex>. Внутри этого цикла создаются два указателя: указатель <tex>j</tex> на начало строки <tex>s_3</tex> и указатель <tex>k</tex> на текущий символ в строке <tex>s_2</tex>, с которым будет производиться сравнение. Затем будем в цикле пытаться добавить символ <tex>s[j]</tex> к строке <tex>s_2</tex>, для чего необходимо произвести сравнение с символом <tex>s[k]</tex>. При этом будем поддерживать инвариант: <tex>j - k</tex> {{---}} длина подстроки w. |
| − | + | Возникают три различных случая: | |
| − | + | 1. Если <tex>s[j] = s[k]</tex>, то мы можем дописать символ <tex>s[j]</tex> к строке <tex>s_2</tex>, не нарушив её "предпростоты". Следовательно, в этом случае мы просто увеличиваем указатели <tex>j</tex> и <tex>k</tex> на единицу, сохраняя инвариант. | |
| − | 3. Если <tex>s[j] < s[k]</tex>, то строка <tex>s_2 + s[j]</tex> уже не может быть предпростой. Поэтому мы разбиваем предпростую строку <tex>s_2</tex> на простые строки плюс "остаток" (префикс простой строки, возможно, пустой); простые строки добавляем в ответ (т.е. выводим их позиции, попутно передвигая указатель <tex>i</tex>), а "остаток" вместе с символом <tex>s[j]</tex> переводим обратно в строку <tex>s_3</tex>, и останавливаем выполнение внутреннего цикла. Тем самым мы на следующей итерации внешнего цикла заново обработаем остаток, зная, что он не мог образовать предпростую строку с предыдущими простыми строками. Осталось только заметить, что при выводе позиций простых строк нам нужно знать их длину; но она | + | 2. Если <tex>s[j] > s[k]</tex>, то, очевидно, строка <tex>s_2 + s[j]</tex> станет простой. Тогда мы увеличиваем <tex>j</tex> на единицу, а <tex>k</tex> передвигаем обратно на <tex>i</tex>, чтобы следующий символ сравнивался с первым символом <tex>s_2</tex>. То есть получаем новую простую строку длины <tex>j - k</tex>. |
| + | |||
| + | 3. Если <tex>s[j] < s[k]</tex>, то строка <tex>s_2 + s[j]</tex> уже не может быть предпростой. Поэтому мы разбиваем предпростую строку <tex>s_2</tex> на простые строки плюс "остаток" (префикс простой строки, возможно, пустой); простые строки добавляем в ответ (т.е. выводим их позиции, попутно передвигая указатель <tex>i</tex>), а "остаток" вместе с символом <tex>s[j]</tex> переводим обратно в строку <tex>s_3</tex>, и останавливаем выполнение внутреннего цикла. Тем самым мы на следующей итерации внешнего цикла заново обработаем остаток, зная, что он не мог образовать предпростую строку с предыдущими простыми строками. Осталось только заметить, что при выводе позиций простых строк нам нужно знать их длину; но она равна <tex>j - k</tex>. | ||
===Реализация=== | ===Реализация=== | ||
| Строка 113: | Строка 115: | ||
===Корректность=== | ===Корректность=== | ||
| + | Покажем, что алгоритм получает нужное разложение. То есть все <tex>s_i</tex> - простые и <tex>s_1 \geqslant s_2 \geqslant ... \geqslant s_k</tex> лексикографически. | ||
| + | |||
| + | При обработке текущего символа в первом случае просто сдвигаем указатели, не записывая ответ. Во втором случае объединяем все найденные <tex>w</tex> с <tex>w'</tex> и получем новую строку <tex>w''</tex>. | ||
| + | |||
| + | Покажем, что <tex>w''</tex> является простой. Рассмотрим ее суффикс. Если он начинается в середине <tex>w</tex>, сравним его посимвольно со строкой <tex>s_2</tex>, и тогда в каком-то символе он окажется больше <tex>s_2</tex>, так как суффикс начинается с <tex>w</tex>, а строка <tex>w</tex> {{---}} простая и по определению меньше всех своих суффиксов. Если суффикс начинается в <tex>w'</tex>, то при сравнении расхождение будет в символах <tex>s[j]</tex> и <tex>s[k]</tex>. Но <tex>s[j] > s[k]</tex>, так что суффикс меньше <tex>w''</tex>. Если же суффикс начинается в начале <tex>w</tex>, отбросим общий префикс вида <tex>w + w + ... + w</tex> и придем к предыдущему случаю. | ||
| + | |||
| + | В третьем случае просто выведем все <tex>w</tex> и продолжим обработку со строки <tex>w'</tex>, так как при добавлении <tex>s[j] s_2</tex> перестанет удовлетворять требованиям, ведь в этом случае суффикс <tex>w'</tex> будет меньше <tex>w</tex>. | ||
| + | |||
| + | Теперь покажем, что <tex>s_i \geqslant s_{i + 1}</tex>. | ||
| + | Последоваельность из <tex>w</tex> будет удовлетворять условию, так как эти строки равны. Следующее слово будет иметь общий префикс с <tex>w</tex>, а после него будет стоять символ, меньший следующего символа из <tex>w</tex> (новое <tex>w</tex> получается по третьему случаю). | ||
===Асимптотика=== | ===Асимптотика=== | ||
Версия 19:16, 4 мая 2014
Декомпозиция Линдона была изобретена Роджером Линдоном (англ. Roger Lyndon) в 1954 году. Она используется для нахождения лексикографически минимального и максимального суффиксов строки, а также лексикографически минимального циклического сдвига.
Содержание
Основные определения
| Определение: |
| Простая строка — строка, которая лексикографически меньше любого своего суффикса. |
Примеры:
— простая строка, так как , , , .
— не простая строка, так как .
| Определение: |
| Декомпозиция Линдона (англ. Lyndon decomposition) строки — её разложение , где строки просты, и при этом . |
Существование и единственность
| Лемма: |
, — простые и лексикографически. Тогда верны следующие утверждения:
1. 2. — простая |
| Доказательство: |
|
1. Так как , то и , 2. Пусть — суффикс строки . Тогда рассмотрим возможных случая:
|
| Теорема (Чен-Линдон-Фокс): |
Можно построить декомпозицию Линдона любой строки , причем единственным образом. |
| Доказательство: |
|
1. Существование. У каждой строки существует хотя бы одно разбиение на простые слова. Это следует из того, что отдельный символ является простым словом. Тогда среди всех разбиений строки на простые слова возьмём то, в котором меньше всего слов. Покажем, что это и будет декомпозицией Линдона данной строки. Предположим, что это не так. Значит, . Так как слова и простые, то из доказанной леммы следует, что эти слова можно сконкатенировать и получить разбиение строки на меньшее число слов. Получили противоречие. Таким образом доказали даже более сильное утверждение: , — минимально нет 2. Единственность. Пусть существует несколько разбиений , удовлетворяющих условию теоремы. Сравним длины первых двух слов и , если , сравним вторые и так далее. Если у всех слов длины одинаковы, то разбиения совпадают — противоречие. Иначе . Покажем, что такого не может быть: 1) Пусть , тогда , где — префикс , . Получаем: , так как простая и по определению меньше своего суффикса, (так как — префикс ???), (по условию разбиения), (их начало совпадает, и по предположению. Получили противоречие: . 2) Случай симметричен разобранному. То есть не может быть строк несовпадающей длины, значит, разбиения равны. |
Алгоритм Дюваля
Алгоритм
Алгоритм Дюваля (англ. Duval's algorithm) находит для данной строки длины декомпозицию Линдона за время с использованием дополнительной памяти. Он строит декомпозицию только на упорядоченных алфавитах. Алгоритм Дюваля относится к классу жадных алгоритмов.
| Определение: |
| Предпростая строка — строка , такая что , где — некоторая простая строка, а — некоторый префикс строки . |
Во время работы алгоритма строка разделена на три строки , где в строке декомпозиция Линдона уже найдена и уже больше не используется алгоритмом; строка — это предпростая строка (причём длину простых строк внутри неё мы также запоминаем); строка — это ещё не обработанная часть строки . Алгоритм Дюваля берёт первый символ строки и пытается дописать его к строке . При этом, возможно, для какого-то префикса строки декомпозиция Линдона становится известной, и эта часть переходит к строке .
Будем поддерживаться указатель на начало строки . Внешний цикл алгоритма будет выполняться, пока , то есть пока вся строка не перейдёт в строку . Внутри этого цикла создаются два указателя: указатель на начало строки и указатель на текущий символ в строке , с которым будет производиться сравнение. Затем будем в цикле пытаться добавить символ к строке , для чего необходимо произвести сравнение с символом . При этом будем поддерживать инвариант: — длина подстроки w.
Возникают три различных случая:
1. Если , то мы можем дописать символ к строке , не нарушив её "предпростоты". Следовательно, в этом случае мы просто увеличиваем указатели и на единицу, сохраняя инвариант.
2. Если , то, очевидно, строка станет простой. Тогда мы увеличиваем на единицу, а передвигаем обратно на , чтобы следующий символ сравнивался с первым символом . То есть получаем новую простую строку длины .
3. Если , то строка уже не может быть предпростой. Поэтому мы разбиваем предпростую строку на простые строки плюс "остаток" (префикс простой строки, возможно, пустой); простые строки добавляем в ответ (т.е. выводим их позиции, попутно передвигая указатель ), а "остаток" вместе с символом переводим обратно в строку , и останавливаем выполнение внутреннего цикла. Тем самым мы на следующей итерации внешнего цикла заново обработаем остаток, зная, что он не мог образовать предпростую строку с предыдущими простыми строками. Осталось только заметить, что при выводе позиций простых строк нам нужно знать их длину; но она равна .
Реализация
string s // входная строка string[] words // декомпозиция n |s| i 0 cur 0 while i n: j i + 1 k i while j n and s[k] s[j]: if s[k] s[j]: k i else: k k + 1 j j + 1 while i k: words[cur] s[i..j - k] cur w + 1 i i + j - k;
Корректность
Покажем, что алгоритм получает нужное разложение. То есть все - простые и лексикографически.
При обработке текущего символа в первом случае просто сдвигаем указатели, не записывая ответ. Во втором случае объединяем все найденные с и получем новую строку .
Покажем, что является простой. Рассмотрим ее суффикс. Если он начинается в середине , сравним его посимвольно со строкой , и тогда в каком-то символе он окажется больше , так как суффикс начинается с , а строка — простая и по определению меньше всех своих суффиксов. Если суффикс начинается в , то при сравнении расхождение будет в символах и . Но , так что суффикс меньше . Если же суффикс начинается в начале , отбросим общий префикс вида и придем к предыдущему случаю.
В третьем случае просто выведем все и продолжим обработку со строки , так как при добавлении перестанет удовлетворять требованиям, ведь в этом случае суффикс будет меньше .
Теперь покажем, что .
Последоваельность из будет удовлетворять условию, так как эти строки равны. Следующее слово будет иметь общий префикс с , а после него будет стоять символ, меньший следующего символа из (новое получается по третьему случаю).
Асимптотика
Дополнительная память требуется только на три указателя: .
Внешний цикл делает не более итераций, поскольку в конце каждой его итерации к результату добавляется как минимум один символ (а всего символов ).
Оценим теперь количество итераций первого вложенного цикла . Для этого рассмотрим второй вложенный цикл — он при каждом своём запуске выводит некоторое количество копий одной и той же простой строки некоторой длины . Заметим, что строка является предпростой, причём её простые строки имеют длину как раз , т.е. её длина не превосходит . Поскольку длина строки равна , а указатель увеличивается по единице на каждой итерации первого вложенного цикла , то этот цикл выполнит не более итераций. Худшим случаем является случай , и мы получаем, что первый вложенный цикл всякий раз выполняет не более итераций. Вспоминая, что всего выводится символов, получаем, что для вывода символов требуется не более итераций первого вложенного . Следовательно, алгоритм Дюваля выполняется за .
Легко оценить и число сравнений символов, выполняемых алгоритмом Дюваля. Поскольку каждая итерация первого вложенного цикла производит два сравнения символов, а также одно сравнение производится после последней итерации цикла, то общее число сравнений символов не превосходит .