Uploaded by Шахобиддин Ахмадалиев

okhotin algorithms 2019 l7

advertisement
Математические основы алгоритмов, осень 2019 г.
Лекция 7: B-деревья. Строковые алгоритмы.
Полиномиальное хэширование: алгоритм Рабина–Карпа,
наибольшая общая подстрока, самый длинный палиндром∗
Александр Охотин
1 июля 2020 г.
Содержание
1 B-деревья
1.1 Вставка в B-дереве . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Удаление в B-дереве . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Понятие о красно-чёрных деревьях . . . . . . . . . . . . . . . . . . . . . . . . .
1
2
3
4
2 Полиномиальное хэширование
2.1 Хэш-функции для строк . . .
2.2 Алгоритм Рабина–Карпа . . .
2.3 Наибольшая общая подстрока
2.4 Самый длинный палиндром .
4
4
5
5
6
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
B-деревья
Двоичные деревья поиска рассчитаны на хранение в оперативной памяти компьютера, позволяющей за одну операцию обратиться не более чем к нескольким байтам. Каждая вершина дерева может быть обработана за несколько таких операций, и такое использование
оперативной памяти оптимально.
Для хранения деревьев во внешней, медленной памяти (такой, как жёсткий диск) структуру данных необходимо адаптировать. Главная особенность внешней памяти в том, что за
одну операцию читается или записывается блок данных размером в несколько килобайт
— например, сектор на жёстком диске. Поэтому для доступа к одной вершине двоичного
дерева пришлось бы работать с целым блоком, и поиск в дереве с n вершинами потребовал
бы порядка log2 n операций с блоками. Это неоптимально.
Предложенные Байером и Маккрайтом [1972] B-деревья — это адаптация деревьев поиска для хранения во внешней памяти. Главная мысль — использовать вершины большой
степени — с тем, чтобы каждая вершина занимала один блок, а высота дерева уменьшилась
бы. Например, если все вершины имеют степень 1000, то высота дерева с миллиардом вершин будет равна всего лишь трём (а не тридцати, как у двоичного дерева), и поиск нужного
листа потребует прочитать лишь 4 блока.
∗
Краткое содержание лекций, прочитанных студентам 1-го курса СПбГУ, обучающимся по программам «Математика» и «МАиАД», в осеннем семестре 2019–2020 учебного года. Страница курса:
http://users.math-cs.spbu.ru/~okhotin/teaching/algorithms_2019/.
1
Рис. 1: Рудольф Байер (род. 1939), Эдвард Маккрайт.
Пусть вершины в двоичном дереве — это «2-вершины», поскольку у каждой из них 2
потомка и 1 значение, по которому эти потомки разделяются. У m-вершины — m потомков
(деревья t1 , . . . , tm — возможно, пустые), и в ней находится m − 1 значение: x1 , . . . , xm−1 ,
где x1 6 . . . 6 xm−1 . Все значения в каждом поддереве ti больше или равны xi и меньше
или равны xi+1 , как показано на рис. 2.
В B-дереве могут одновременно содержаться вершины различных степеней: выбирается
некоторое число k > 2, после чего корень может иметь степень от 0 до 2k, а все остальные
вершины — любые степени от k до 2k. При этом дерево сбалансировано: длины всех путей
равны.
x1 ... xi –1
x1
xi –1
t1
xi ... xm –1
xi
xm –1
ti
tm
Рис. 2: m-вершина в B-дереве
При поиске элемента x в B-дереве, на каждом шаге рассматривается некоторая mвершина, в которой размещены значения x1 , . . . , xm−1 . В этом массиве запускается двоичный поиск элемента x. Если x находится в массиве, то поиск в дереве на этом завершён,
а если нет, то двоичный поиск указывает на поддерево ti , для которого верно xi−1 < x < xi ,
и потому элемент x может находиться только в нём. Поиск продолжается в поддереве ti .
1.1
Вставка в B-дереве
В АВЛ-дереве вставка и удаление начинается с поиска данного элемента, после чего исправляется возможная разбалансировка. В худшем случае придётся пройти путь от корня
к листу, а потом обратно от листа к корню.
В B-дереве вставка и удаление делаются иначе: исправление потенциальной разбалансировки начинается уже на этапе поиска удаляемого элемента в дереве.
Пусть вставка или удаление производятся в листе, и это m-вершина. Тогда, если этот
лист заполнен не до конца (m < 2k), то в нём найдётся место для дополнительного значения,
а если он заполнен не минимально (m > k), то из него можно удалить любое из его значений.
При вставке, спускаясь вниз по дереву, нужно разделять каждую очередную встреченную «полную» 2k-вершину на две k-вершины — за счёт её сестёр или родительницы. При
2
этом одно лишнее значение выталкивается на один уровень выше, как на рис. 3. Поскольку
на уровне выше не может быть 2k-вершины (ведь алгоритм только что оттуда спустился),
в ней есть место, в которое можно вытолкнуть лишнее значение. В итоге найденный лист
тоже окажется степени не более чем 2k − 1 — то есть, в нём будет не более чем 2k − 1 пустых указателей на несуществующих потомков, и между ними не более чем 2k −2 значений;
следовательно, в этом листе найдётся, куда вставить новое значение.
y
xyz
t1
t2
x
t3
t4
z
t1
t2
t3
t4
Рис. 3: Разделение вершины при вставке в B-дереве, для k = 2: (слева) переполненная
4-вершина; (справа) выталкивание переполнения наверх.
Что делать, если при вставке окажется, что степень корня — 2k? Здесь важно, что степень корня не ограничена снизу. Тогда корень точно так же разделяется на две k-вершины,
лишнее значение так же выталкивается на уровень выше, где появляется новый корень
степени 2, с одним значением. Высота дерева увеличивается только в этом случае.
1.2
Удаление в B-дереве
При удалении, спускаясь вниз, нужно точно так же увеличивать каждую встреченную kвершину за счёт её сестёр. Для всякой встреченной k-вершины уже обеспечено, что её
родительница — не менее чем (k + 1)-вершина. Значит, у текущей k-вершины есть не менее
k сестёр.
• Если одна из соседних сестёр — не менее чем (k + 1)-вершина, то недостающее поддерево перегоняется из неё, как показано на рис. 4.
y
z
z
x
x
y
...
...
t
t
Рис. 4: Заимствование поддерева у соседней сестры при удалении в B-дереве: (слева) малоимущая k-вершина, выделенная красным; (справа) поддерево заимствовано у сестры справа.
• Если же соседняя сестра — k-вершина, то она объединяется с текущей k-вершиной в
одну 2k-вершину — это в точности обратная операция к разделению вершины, изображённому на рис. 3. При этом у родительницы станет на одно поддерево меньше, но
она это переживёт, поскольку она не k-вершина.
3
Когда же наконец находится вершина, содержащая удаляемое значение, если это лист, то
значение просто удаляется (он же степени хотя бы k + 1!). Если же нужно удалить элемент,
находящийся во внутренней вершине, то запоминается указатель на эту вершину, после
чего делается следующее.
• Если в предшествующем значению поддереве, в его корне есть хотя бы k + 1 значение,
то в поддереве находится значение-предшественник, и процедура стирания продолжается уже для него (а само оно замещает стираемый элемент).
• Аналогично в следующем поддереве.
• Если же у соседних поддеревьев по k значений, то они объединяются в одно с 2k
значениями.
Корень нужно рассмотреть особо. Поскольку его степень не ограничена снизу, сам по
себе он не нуждается в исправлении. Однако если корень — 2-вершина, а оба его потомка —
k-вершины, то исправление этих k-вершин приведёт к полному опустошению корня. В этом
случае все три вершины объединяются в одну 2k-вершину, а высота дерева уменьшается на
единицу.
1.3
Понятие о красно-чёрных деревьях
B-дерево для k = 2 называется 2-3-4-деревом, и такое дерево удобно хранить в оперативной памяти. Оно работает незначительно быстрее АВЛ-дерева, однако требует огромного
объёма кода для своей реализации.
Красно-чёрное дерево — это представление 2-3-4-дерева, в котором каждая 3-вершина
разбита на две двоичных, а каждая 4-вершина — на три двоичных, и они определённым
образом раскрашены в два цвета. Каждая операция над 2-3-4-деревом представляется в
виде нескольких более простых операций над красно-чёрными деревьями. Поэтому красночёрные деревья существенно удобнее программировать, и они успешно используются на
практике.
2
Полиномиальное хэширование
Строки над алфавитом Σ, подстроки, префиксы, суффиксы.
Задача 1 (Поиск в строке). Дана длинная строка («текст») w = a1 . . . an , и короткая искомая строка («шаблон») x = b1 . . . bm . Требуется найти все вхождения x в w в качестве
подстроки, то есть, все смещения s, для которых подстрока ws = as+1 . . . as+m совпадает
с b1 . . . bm .
«Наивный» алгоритм: сравнивать все m символов для каждого s, время O(mn). В худшем случае достигается, пример: x = 0m−1 1 и w = 02m−1 1, всего m2 сравнений.
Но можно искать быстрее.
2.1
Хэш-функции для строк
Каждой строке ставится в соответствие число: w
h(w).
Пусть ключи в таблице — это строки над алфавитом Σ. Как лучше определить хэшфункцию?
4
Самое простое, что можно придумать: сложить коды всех символов, взять сумму по
модулю размера таблицы. Недостаток: распределение кодов неравномерно! Поэтому и сумма будет распределена неравномерно, и существенная часть элементов таблицы не будет
использоваться, а строки, совпадающие с точностью до порядка символов, получат одинаковые значения. Это совсем плохо.
Полиномиальное хэширование: берётся
P некоторое основание степени p. Пусть w = a1 . . . a`
— строка. Тогда используется сумма `i=1 ai · p`−i , взятая по некоторому модулю M .
2.2
Алгоритм Рабина–Карпа
Алгоритм, предложенный Карпом и Рабином [1987] основан на полиномиальном хэшировании степени m − 1: сперва вычисляется значение хэш-функции для искомой строки, а затем
для всех m-символьных подстрок данного текста последовательно вычисляется значение их
хэш-функции. Когда значение для подстроки совпадает со значением для искомой строки,
алгоритм проводит прямое сравнение символов, как в «наивном» алгоритме.
Рис. 5: Майкл Рабин (род. 1931) и Ричард Карп (род. 1935).
P
m−i
Значение хэш-функции для искомой строки: X = m
i=1 bi · p P по модулю q.
m−i по модулю
Значение хэш-функции для подстроки со смещением s: Ws = m
i=1 as+i · p
q.
Алгоритм сперва вычисляет X, а затем последовательно вычисляет Ws для s от 0 до
n − m. Удобство этой хэш-функции в том, что каждое следующее значение Ws+1 можно
вычислить на основе значения Ws за O(1) шагов. Для этого достаточно заметить, что числа
pWs и Ws+1 отличаются всего на два слагаемых.
pWs = as+1 pm + as+2 pm−1 + . . . + as+m p
Ws+1 = as+2 pm−1 + . . . + as+m p + as+m+1
Отсюда Ws+1 получается из Ws по следующей формуле (вся арифметика — по модулю q).
Ws+1 = p · Ws − as+1 · pm + as+m+1
Сложность: Θ(m) на подготовку, и затем в худшем случае O(mn), если хэш-функция
выдаст одинаковые значения для всех подстрок. Это неизбежно, если, например, все подстроки одинаковы, то есть, w = an и x = am .
Но в среднем случае получается время работы Θ(n).
5
2.3
Наибольшая общая подстрока
Полиномиальное хэширование позволяет легко построить относительно неплохие, пусть и
не самые оптимальные, алгоритмы решения ряда задач.
Задача 2 (Наибольшая общая подстрока). Дано: u = a1 . . . am и v = b1 . . . bn . Найти самую
длинную строку x, для которой u = u1 xu2 и v = v1 xv2 .
Наивное решение: для каждой пары позиций найти самую длинную подстроку, время
O((m + n)3 ).
Как можно сделать это лучше? Сперва с помощью полиномиального хэширования надо научиться отвечать на вопрос «Есть ли общая подстрока длины `?» за время O((m +
n) log(m + n)), Для этого находятся значения хэш-функции для всех подстрок u длины
`, размещаются в двоичном дереве поиска — время m log m. То же самое — для v, время
n log n. Далее исследуются совпадающие значения.
Наконец, используется двоичный поиск по `. Его можно записать в виде рекурсивной
процедуры, отвечающей на вопрос «Найти длину наибольшей общей подстроки, если известно, что её длина не меньше, чем `1 , и строго меньше, чем `2 ?» (алгоритм 1). А можно
то же самое сделать и без рекурсии.
Алгоритм 1 Нахождение длины наибольшей общей подстроки двоичным поиском
процедура f (`1 , `2 )
1: if `2 − `1 = 1 then
2:
`return
`1
1 +`2
3: k =
2
4: if есть общая подстрока длины k then
5:
return f (k, `2 )
6: else
7:
return f (`1 , k)
2.4
Самый длинный палиндром
Обращение строки — запись всех символов в обратном порядке: (a1 a2 . . . an−1 an )R = an an−1 . . . a2 a1 .
Если u = uR , такая строка называется палиндромом.
Задача 3 (Самый длинный палиндром). По данной строке w = a1 . . . an найти самую
длинную её подстроку-палиндром.
Наивный алгоритм: искать вокруг каждого символа на длину вплоть до n2 , время O(n2 ).
Улучшенный алгоритм основан на решении подзадачи: «Есть ли подстрока-палиндром
данной длины `?» После того, как будет построен алгоритм для решения подзадачи, останется использовать двоичный поиск по `.
Задачу поиска палиндромов заранее заданной длины ` можно переформулировать так.
Подстрока ai+1 . . . ai+` строки w — палиндром, если она совпадает со строкой ai+` . . . ai+1 ,
которая в свою очередь встречается в строке wR , начиная с позиции n − i − ` + 1. Стало
быть, нужно проверять на равенство подстроки длины `, начинающиеся с позиции i + 1 в
w и с позиции n − i − ` + 1 в wR . Это удобно сделать, вычислив значения полиномиального
6
хэша для всех подстрок длиныX w и wR длины `, а затем для каждого i сравнить хэш w
для позиции i + 1 и хэш wR для позиции n − i − ` + 1. Для каждого совпадения придётся
проверять подстроки на равенство посимвольно, но если хэш-функция не подведёт, это
будет требоваться очень редко, так что среднее время будет линейным.
Двоичный поиск по ` даст время O(n log n).
Список литературы
[1972] R. Bayer, E. M. McCreight, “Organization and maintenance of large ordered indexes”,
Acta Informatica, 1:3 (1972), 173–189.
[1978] L. J. Guibas, R. Sedgewick, “A dichromatic framework for balanced trees”, FOCS 1978,
8–21.
[1987] R. M. Karp, M. O. Rabin, “Efficient randomized pattern-matching algorithms”, IBM
Journal of Research and Development, 31:2 (1987), 249–260.
7
Download