Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

сиаод / exam_saod

.pdf
Скачиваний:
60
Добавлен:
11.05.2015
Размер:
958.74 Кб
Скачать

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

Приблизительно в 1970 г. Д. Кнут, Д. Морис и В. Пратт изобрели алгоритм, фактически требующий только N сравнений даже в самом плохом случае. Новый алгоритм основывается на том соображении, что после частичного совпадения начальной части слова с соответствующими символами текста фактически известна пройденная часть текста и можно вычислить некоторые сведения (на основе самого слова), с помощью которых потом можно быстро продвинуться по тексту. Приведенный пример поиска слова ABCABD показывает принцип работы такого алгоритма. Символы, подвергшиеся сравнению, здесь подчеркнуты. Обратите внимание: при каждом несовпадении пары символов слово сдвигается на все пройденное расстояние, поскольку меньшие сдвиги не могут привести к полному совпадению.

Основным отличием КМП-алгоритма от алгоритма прямого поиска является осуществления сдвига слова не на один символ на каждом шаге алгоритма, а на некоторое переменное количество символов. Таким образом, перед тем как осуществлять очередной сдвиг, необходимо определить величину сдвига. Для повышения эффективности алгоритма необходимо, чтобы сдвиг на каждом шаге был бы как можно большим.

Если j определяет позицию в слове, содержащую первый несовпадающий символ (как в алгоритме прямого поиска), то величина сдвига определяется как j-D. Значение D определяется как размер самой длинной последовательности символов слова, непосредственно предшествующих позиции j, которая полностью совпадает с началом слова. D зависит только от слова и не зависит от текста. Для каждого j будет своя величина D, которую обозначим dj.

Так как величины dj зависят только от слова, то перед началом фактического поиска можно вычислить вспомогательную таблицу d; эти вычисления сводятся к некоторой предтрансляции слова. Соответствующие усилия будут оправданными, если размер текста значительно превышает размер слова (M<<N). Если нужно искать многие вхождения одного и того же слова, то можно пользоваться одними и теми же d. Приведенные примеры объясняют функцию d.

Последний пример на рис. 2.2 показывает: так как pj равно A вместо F, то соответствующий символ текста не может быть символом A из-за того, что условие si<>pj заканчивает цикл. Следовательно, сдвиг на 5 не приведет к последующему совпадению, и поэтому можно увеличить размер сдвига до шести. Учитывая это, предопределяем вычисление dj как поиск самой длинной совпадающей последовательности с дополнительным ограничением pdj<>pj. Если никаких совпадений нет,

Рис. 2.2. Частичное совпадение со словом и вычисление d j.

то считается dj=-1, что указывает на сдвиг на целое слово относительно его текущей позиции. Следующая программа демонстрирует КМП-алгоритм.

Program KMP; const

Mmax = 100; Nmax = 10000;

var

i, j, k, M, N: integer;

p: array[0..Mmax-1] of char; {слово} s: array[0..Mmax-1] of char; {текст} d: array[0..Mmax-1] of integer;

begin

{Ввод текста s и слова p}

Write('N:'); Readln(N);

Write('s:'); Readln(s); Write('M:'); Readln(M); Write('p:'); Readln(p);

{Заполнение массива d} j:=0; k:=-1; d[0]:=-1; while j<(M-1) do begin

while(k>=0) and (p[j]<>p[k]) do k:=d[k]; j:=j+1; k:=k+1;

if p[j]=p[k] then d[j]:=d[k]

else

d[j]:=k;

end;

{Поиск слова p в тексте s} i:=0; j:=0;

while (j<M) and (i<N) do begin

while (j>=0) and (s[i]<>p[j]) do j:=d[j]; {Сдвиг слова} i:=i+1; j:=j+1;

end;

{Вывод результата поиска}

if j=M then Writeln('Yes') {найден } else Writeln('No'); {не найден} Readln;

end.

Точный анализ КМП-поиска, как и сам его алгоритм, весьма сложен. Его изобретатели доказывают, что требуется порядка M+N сравнений символов, что значительно лучше, чем M*N сравнений из прямого поиска. Они так же отмечают то положительное свойство, что указатель сканирования i никогда не возвращается назад, в то время как при прямом поиске после несовпадения просмотр всегда начинается с первого символа слова и поэтому может включать символы, которые ранее уже просматривались. Это может привести к негативным последствиям, если текст читается из вторичной памяти, ведь в этом случае возврат обходится дорого. Даже при буферизованном вводе может встретиться столь большое слово, что возврат превысит емкость буфера.

16. Двоичное дерево поиска. Свойства. Основные операции.

Двоичные деревья представляют эффективный способ поиска. Двоичное дерево представляет собой структурированную коллекцию узлов. Коллекция может быть пустой и в этом случае мы имеем пустое двоичное дерево. Если коллекция непуста, то она подразделяется на три раздельных семейства узлов: корневой узел n (или просто корень), двоичное дерево, называемое левым поддеревом для n, и двоичное дерево, называемое правым поддеревом для n. На рис. 1а узел, обозначенный буквой А, является корневым, узел В называется левым потомком А и является корнем левого поддерева А, узел С называется правым потомком А и является корнем правого поддерева А.

Рис. 1: Двоичное дерево с показанными внешними узллами (а) и без них (б)

Двоичное дерево на рис. 1а состоит из четырех внутренних узлов (обозначенных на рис. кружками) и пяти внешних (конечных) узлов (обозначены квадратами). Размер двоичного дерева определяется числом содержащихся в нем внутренних узлов. Внешние узлы соответствуют пустым двоичным деревьям. Например, левый потомок узла В — непустой (содержит узел D), тогда как правый потомок узла В — пустое дерево. В некоторых случаях внешние узлы обозначаются каким-либо образом, в других — на них совсем не ссылаются и они считаются пустыми двоичными деревьями (на рис. 16 внешние узлы не показаны).

Основанная на генеалогии метафора дает удобный способ обозначения узлов внутри двоичного дерева. Узел р является родителем (или предком) узла n, если n — потомок узла р. Два узла являются братьями, если они принадлежат одному и тому же родителю. Для двух заданных узлов n1 и nk таких, что узел nk

принадлежит поддереву с корнем в узле n1, говорят, что узел nk является потомком узла n1, а узел n1 — предком узла nk. Существует уникальный путь от узла n1 вниз к каждому из потомков nk, a именно: последовательность узлов n1 и n2,... nk такая, что узел ni является родителем узла ni+1 для i = 1, 2,..., k- 1. Длина пути равна числу ребер (k-1), содержащихся в нем. Например, на рис. 1а уникальный путь от узла А к узлу D состоит из последовательности А, В, D и имеет длину 2.

Глубина узла n определяется рекурсивно:{ 0 если n — корневой узел глубина (n) = {{ 1 + глубина (родителя (n)) в противном случае

Глубина узла равна длине уникального пути от корня к узлу. На рис. 1а узел А имеет глубину 0, а узел D имеет глубину, равную 2.

Высота узла n также определяется рекурсивно: { 0 если n — внешний узел высота (n) = {{ 1 + max( высота(лев(n)), высота(прав(n)) ) в противном случае

где через лев(n) обозначен левый потомок узла n и через прав(n) — правый потомок узла n. Высота узла n равна длине самого длинного пути от узла n вниз до внешнего узла поддерева n. Высота двоичного дерева определяется как высота его корневого узла. Например, двоичное дерево на рис. 1а имеет высоту 3, а узел D имеет высоту 1.

Основное назначение двоичных деревьев заключается в повышении эффективности поиска. При поиске выполняются такие операции, как нахождение заданного элемента из набора различных элементов, определение наибольшего или наименьшего элемента в наборе, фиксация факта, что набор содержит заданный элемент. Для эффективного поиска внутри двоичного дерева его элементы должны быть организованы соответствующим образом. Например, двоичное дерево будет называться двоичным деревом поиска, если его элементы расположены так, что для каждого элемента n все элементы в левом поддереве n будут меньше, чем n, а все элементы в правом поддереве — будут больше, чем n. На рис. 2 изображены три двоичных дерева поиска, каждое из которых содержит один и тот же набор целочисленных элементов.

Рис. 2: Три двоичных дерева поиска с одним и тем же набором элементов В общем случае существует огромное число двоичных деревьев поиска (различной формы) для любого заданного набора элементов.

Предполагается, что элементы располагаются в линейном порядке и, следовательно, любые два элемента можно сравнить между собой. Примерами линейного порядка могут служить ряды целых или вещественных чисел в порядке возрастания, а также слов или строк символов, расположенных в лексикографическом (алфавитном, или словарном) порядке. Поиск осуществляется путем обращения к функции сравнения для сопоставления любых двух элементов относительно их линейного порядка. В нашей версии деревьев поиска действие функции сравнения ограничено только явно определенными объектами деревьев поиска.

Также очень полезны функции для обращения к элементам дерева поиска и воздействия на них. Такие функции обращения могут быть полезны для вывода на печать, редактирования и доступа к элементу или воздействия на него каким-либо иным образом. Функции обращения не принадлежат деревьям поиска, к элементам одного и того же дерева поиска могут быть применены различные функции обращения.

Основные операции в двоичном дереве поиска

Базовый интерфейс двоичного дерева поиска состоит из трех операций:

FIND(K) — поиск узла, в котором хранится пара (key, value) с key = K. INSERT(K,V) — добавление в дерево пары (key, value) = (K, V).

REMOVE(K) — удаление узла, в котором хранится пара (key, value) с key = K.

Этот абстрактный интерфейс является общим случаем, например, таких интерфейсов, взятых из прикладных задач:

«Телефонная книжка» — хранилище записей (имя человека, его телефон) с операциями поиска и удаления записей по имени человека, и операцией добавления новой записи.

Domain Name Server — хранилище пар (доменное имя, IP адрес) с операциями модификации и поиска. Namespace — хранилище имен переменных с их значениями, возникающее в трансляторах языков программирования.

По сути, двоичное дерево поиска — это структура данных, способная хранить таблицу пар (key, value) и поддерживающая три операции: FIND, INSERT, REMOVE.

Кроме того, интерфейс двоичного дерева включает ещё три дополнительных операции обхода узлов дерева: INFIX_TRAVERSE, PREFIX_TRAVERSE и POSTFIX_TRAVERSE. Первая из них позволяет обойти узлы дерева в порядке неубывания ключей.

17. Добавление элемента в двоичное дерево поиска.

Для включения нового элемента в двоичное дерево поиска вначале нужно определить его точное положение — а именно внешний узел, который должен быть заменен путем отслеживания пути поиска элемента, начиная с корня. Кроме сохранения указателя n на текущий узел мы будем хранить указатель р на предка узла n. Таким образом, когда n достигнет некоторого внешнего узла, р будет указывать на узел, который должен стать предком нового узла. Для осуществления включения узла мы создадим новый узел, содержащий новый элемент, и затем свяжем предок р с этим новым узлом (рис. 3).

Компонентная функция insert включает элемент val в это двоичное дерево поиска:

Добавление элемента (INSERT)

Дано: дерево Т и пара (K,V).

Задача: добавить пару (K, V) в дерево Т. Алгоритм:

Если дерево пусто, заменить его на дерево с одним корневым узлом ((K,V), null, null) и остановиться. Иначе сравнить K с ключом корневого узла X.

Если K>=X, рекурсивно добавить (K,V) в правое поддерево Т. Если K<X, рекурсивно добавить (K,V) в левое поддерево Т.

18. Удаление элемента в двоичном дереве поиска.

Удаление элемента из двоичного дерева поиска гораздо сложнее, чем включение, поскольку может быть значительно изменена форма дерева. Удаение узла, у которого есть не более чем один непустой потомок, является равнительно простой задачей — устанавливается ссылка от предка узла на потомок. Однако ситуация становится гораздо сложнее, если у подлежащего удалению узла есть два непустых потомка: предок узла может быть связан с одним из потомков, а что делать с другим? Решение может заключаться не в удалении узла из дерева, а скорее в замене элемента, содержащегося в нем, на последователя этого элемента, а затем в удалении узла, содержащего этот последователь.

Рис. 4: Три ситуации, возникающие при удалении элемента из двоичного дерева поиска Чтобы удалить элемент из дерева поиска, вначале мы отслеживаем путь поиска элемента, начиная с

корня и вниз до узла n, содержащего элемент. В этот момент могут возникнуть три ситуации, показанные на рис. 4:

Узел n имеет пустой левый потомок. В этом случае ссылка на n (записанная в предке n, если он есть) заменяется на ссылку на правого потомка n.

У узла n есть непустой левый потомок, но правый потомок пустой. В этом случае ссылка вниз на n заменяется ссылкой на левый потомок узла n.

Узел n имеет два непустых потомка. Найдем последователя для n (назовем его m), скопируем данные, хранящиеся в m, в узел n и затем рекурсивно удалим узел m из дерева поиска.

Очень важно проследить, как будет выглядеть результирующее двоичное дерево поиска в каждом случае. Рассмотрим случай 1. Если подлежащий удалению узел n, является левым потомком, то элементы, относящиеся к правому поддереву n будут меньше, чем у узла р, предка узла n. При удалении узла n его правое поддерево связывается с узлом р и элементы, хранящиеся в новом левом поддереве узла р конечно остаются меньше элемента в узле р. Поскольку никакие другие ссылки не изменяются, то дерево остается двоичным деревом поиска. Аргументы остаются подобными, если узел n является правым потомком, и они тривиальны, если n — корневой узел. Случай 2 объясняется аналогично. В случае 3 элемент v, записанный в узле n, перекрывается следующим большим элементом, хранящимся в узле m (назовем его w), после чего элемент w удаляется из дерева. В получающемся после этого

двоичном дереве значения в левом поддереве узла n будут меньше w, поскольку они меньше v. Более того, элементы в правом поддереве узла n больше, чем w, поскольку (1) они больше, чем v, (2) нет ни одного элемента двоичного дерева поиска, лежащего между v и w и (3) из них элемент w был удален.

Заметим, что в случае 3 узел m должен обязательно существовать, поскольку правое поддерево узла n непустое. Более того, рекурсивный вызов для удаления m не может привести к срыву рекурсивного вызова, поскольку если узел m не имел бы левого потомка, то был бы применен случай 1, когда его нужно было бы удалить.

На рис. 5 показана последовательность операций удаления, при которой возникают все три ситуации. Напомним, что симметричный обход каждого дерева в этой последовательности проходит все узлы в возрастающем порядке, проверяя, что в каждом случае это двоичные деревья поиска.

Компонентная функция remove является общедоступной компонентной функцией для удаления узла, содержащего заданный элемент. Она обращается к собственной компонентной функции _remove, которая выполняет фактическую работу:

Рис. 5: Последовательность операций удаления элемента: (а) и (б) — Случай 1: удаление из двоичного дерева элемента 8; (б) и (в) — Случай 2: удаление элемента 5; (в) и (г) - Случай 3: удаление элемента 3

Удаление узла (REMOVE)

Дано: дерево Т с корнем n и ключом K.

Задача: удалить из дерева Т узел с ключом K (если такой есть).Алгоритм:Если дерево T пусто, остановиться

Иначе сравнить K с ключом X корневого узла n.

Если K>X, рекурсивно удалить K из правого поддерева Т.

Если K<X, рекурсивно удалить K из левого поддерева Т. Если K=X, то необходимо рассмотреть три случая.

Если обоих детей нет, то удаляем текущий узел и обнуляем ссылку на него у родительского узла.

Если одного из детей нет, то значения полей второго ребёнка m ставим вместо соответствующих значений корневого узла, затирая его старые значения, и освобождаем память, занимаемую узлом m. Если оба ребёнка присутствуют, то

найдём узел m, являющийся самым левым узлом правого поддерева; скопируем значения полей (key, value) узла m в соответствующие поля узла n.

у родителя узла m заменим ссылку на узел m ссылкой на правого ребёнка узла m (который, в принципе, может быть равен null).

освободим память, занимаемую узлом m (на него теперь никто не указывает, а его данные были перенесены в узел n).

19 AVLдеревья. Свойства. Вращение.

Так называемые AVL-деревья (названные в честь их двух изобретателей Г.М. Адельсона-Вельского и Е.М. Ландиса) хранят дополнительно в каждой вершине разность между высотами левого и правого поддеревьев, которая в сбалансированном дереве может принимать только три значения: -1, 0, 1. Строго говоря, AVL-деревья не являются сбалансированными в смысле приведенного выше определения. Требуется только, чтобы для любой вершины AVL-дерева разность высот ее левого и правого поддеревьев была по абсолютной величине не больше единицы. При этом длины путей от корня к внешним вершинам могут различаться больше, чем на единицу. Можно, тем не менее, доказать, что и в случае AVL-деревьев их высота оценивается сверху логарифмически в зависимости от числа вершин:

h <= C log2 n

где константа C = 1.5. Обычно константы не очень важны в практическом программировании — принципиально лишь, по какому закону увеличивается время работы алгоритма при увеличении n. В данном случае зависимость логарифмическая, т.е. наилучшая из всех возможных (поскольку поиск невозможен быстрее чем за log2 n операций).

Новый элемент всегда добавляется в дерево в соответствии с упорядоченностью как левый или правый сын некоторой вершины, у которой данного сына до этого не было (или, как мы считаем, сын являлся внешним). Новая вершина добавляется как терминальная. После этого выполняется процедура восстановления балансировки. В ней используются следующие элементарные преобразования дерева, сохраняющие упорядоченность вершин:

вращение вершины x поддерева влево:

Здесь вершина x поддерева, которая является его корнем, опускается вниз и влево. Бывший правый сын d вершины x становится новым корнем поддерева, а x становится левым сыном d. (Вершины x и d, начальник и подчиненный, как бы меняются ролями: бывший начальник становится подчиненным.) Поддерево c, которое было левым сыном вершины d, переходит в подчинение от вершины d к вершине x и становится ее правым сыном. Отметим, что упорядоченность вершин сохраняется: a < b < c< d < e. Таким образом, для выполнения преобразования надо лишь заменить фиксированное количество указателей в вершинах x, d, c и, возможно, в родительской для x вершине;

вращение вершины x поддерева вправо:

Здесь вершина x опускается вниз и вправо, ее бывший левый сын b становится новым корнем поддерева, а x — его правым сыном. Поддерево c переходит в подчинение от b к x.

Операции вращения носят локальный характер и позволяют при необходимости исправить баланс поддерева с корнем x. Например, для восстановления баланса дерева, показанного на следующем рисунке, достаточно выполнить одно вращение вершины b влево:

В случае AVL-деревьев операции вращения повторяются в цикле при восстановлении баланса после добавления или удаления элемента, число вращений не превышает С · h, где h — высота дерева, C — константа. Таким образом, как поиск элемента, так и его добавление или удаление выполняется за логарифмическое время: t <= C · log2n.

20 Добавление вершины в AVL – дереве.

Алгоритм AVL-вставки

Процесс вставки почти такой же, что и для бинарного дерева поиска. Осуществляется рекурсивный спуск по левым и правым сыновьям, пока не встретится пустое поддерево, а затем производится пробная вставка нового узла в этом месте. В течение этого процесса мы посещаем каждый узел на пути поиска от корневого к новому элементу.

Поскольку процесс рекурсивный, обработка узлов ведется в обратном порядке. При этом показатель сбалансированности родительского узла можно скорректировать после изучения эффекта от добавления нового элемента в одно из поддеревьев. Необходимость корректировки определяется для каждого узла, входящего в поисковый маршрут. Есть три возможных ситуации. В двух первых случаях узел сохраняет сбалансированность и реорганизация поддеревьев не требуется. Нужно лишь скорректировать показатель сбалансированности данного узла. В третьем случае разбалансировка дерева требует одинарного или двойного поворотов узлов.

Случай 1. Узел на поисковом маршруте изначально является сбалансированным. После вставки в поддерево нового элемента узел стал перевешивать влево или вправо в зависимости от того, в какое поддерево была произведена вставка. Если элемент вставлен в левое поддерево, показателю сбалансированности присваивается -1, а если в правое, то 1. Например, на пути 40-50-60 каждый узел сбалансирован. После вставки узла 55 показатели сбалансированности изменяются (рисунок 6).

Рис. 6.

Случай 2. Одно из поддеревьев узла перевешивает, и новый узел вставляется в более легкое поддерево. Узел становится сбалансированным. Сравните, например, состояния дерева до и после вставки узла 55 (рисунок 7).

Случай 3. Одно из поддеревьев узла перевешивает, и новый узел помещается в более тяжелое поддерево. Тем самым нарушается условие сбалансированности, так как balanceFactor выходит за пределы -1..1. Чтобы восстановить равновесие, нужно выполнить поворот.

Рассмотрим пример. Предположим, дерево разбалансировалось слева и мы восстанавливаем равновесие, вызывая одну из функций поворота вправо. Разбалансировка справа влечет за собой симметричные действия.

21. Удаление вершины в AVLдереве.

Очевидно, удаление вершины - процесс намного более сложный, чем добавление. Хотя алгоритм операции балансировки остаётся тем же самым, что и при включении вершины. Балансировка попрежнему выполняется с помощью одного из четырёх уже рассмотренных поворотов вершин.

Удаление из АВЛ-дерева происходит следующим образом. Удалим вершину так же, как это делалось для СДП. Затем двигаясь назад от удалённой вершины к корню дерева, будем восстанавливать баланс в каждой вершине (с помощью поворотов). При этом нарушение баланса возможно в нескольких вершинах в отличие от операции включения вершины в дерево.

Как и в случае добавления вершин, введём логическую переменную Уменьшение, показывающую уменьшилась ли высота поддерева. Балансировка идёт, только если Уменьшение = ИСТИНА. Это значение присваивается переменной Уменьшение, если обнаружена и удалена вершина или высота поддерева уменьшилась в процессе балансировки.

Введём две симметричные процедуры балансировки, т. к. они будут использоваться несколько раз в алгоритме удаления:

BL - используется при уменьшении высоты левого поддерева,

BR - используется при уменьшении высоты правого поддерева.

Рисунки 46 и 47 иллюстрируют три случая, возникающие при удалении вершины из левого (для BL) или правого (для BR) поддерева, в зависимости от исходного состояния баланса в вершине по адресу p.

Пример Удаления АВЛ дерева вершин В 9 2 4 1 7 E F

22 Красночерные деревья. Свойства. Вращение.

Исторически AVL-деревья, изобретенные в 1962 г., были одной из первых схем реализации почти сбалансированных деревьев. В настоящее время, однако, более популярна другая схема: красно-черные деревья, или RB-деревья, от англ. Red-Black Trees. Красно-черные деревья были введены Р. Байером в 1972 г. В стандартной библиотеке классов языка C++ исполнители множество и нагруженное множество — классы set и map — реализованы именно как красно-черные деревья.

Вместо баланс-фактора, применяемого в AVL-деревьях, RB-деревья используют цвета вершин. Каждая вершина окрашена либо в красный, либо в черный цвет. (В реализации за цвет отвечает логическая переменная.) При этом выполняется несколько дополнительных условий:

каждая внешняя (или нулевая) вершина считается черной; корневая вершина дерева черная; у красной вершины дети черные;

всякий путь от корня дерева к произвольной внешней вершине имеет одно и то же количество черных вершин.

Последний пункт определения означает сбалансированность дерева по черным вершинам.

Ниже приведен пример красно-черного дерева. Черные вершины изображены темно-серым цветом, красные — белым.

Из пункта 3) определения следует, что в произвольном пути от корня к терминальной вершине не может быть двух красных вершин подряд. Это означает, что, поскольку число черных вершин в любом пути одинаково, длины разных путей к терминальным вершинам отличаются не более чем вдвое. Это свойство близко по своей сути к сбалансированности. Несложно показать, что для красно-черного дерева справедлива следующая оценка сверху на высоту дерева в зависимости от числа вершин: h <= 2 log2 (n+1) Из этого следует, что поиск в красно-черном дереве также выполняется за логарифмическое время. Новая вершина добавляется в красно-черное дерево как терминальная после процедуры поиска (этим RB-дерево ничем не отличается от других упорядоченных деревьев). Новая вершина окрашивается в красный цвет. При этом пункт 3) в определении красно-черного дерева может нарушиться. Поэтому после добавления, а также удаления вершины выполняется процедура восстановления структуры красно-черного дерева, играющая ту же роль, что и восстановление балансировки AVL-дерева. Преимущество красно-черных деревьев состоит в том, что процедура

восстановления более простая. Во многих случаях она ограничивается перекрашиванием вершин. В ней также могут выполняться операции вращения вершины влево и вправо, но число вращений может быть не больше двух при добавлении элемента и не больше четырех при удалении. Всего число операций при восстановлении структуры RB-дерева оценивается сверху через высоту дерева: число операций <= K · hгде h — высота дерева, K — константа. Поскольку для высоты RB-дерева справедлива приведенная выше логарифмическая оценка от числа вершин n, получаем оценку число операций <= C log2 n

где C - константа. Таким образом, добавление и удаление элементов выполняется в случае красночерных деревьев за логарифмическое время в зависимости от числа вершин дерева.

34. Задача о наибольшей общей подпоследовательности.

Пусть у нас есть последовательность X = {x1, x2, …, xm}, тогда другая последовательность Z = {z1, z2, …, zk} будет подпоследовательностью X, если существует такая возрастающая последовательность индексов I = {i1, i2, …, im}, что для всех j = 1, 2, …, k, будет верно равенство xij = zj.

Пример. Z = {B, C, D, B} — подпоследовательность X = {A, B, C, B, D, A, B} с набором индексов {2, 3,

5, 7}.

Говорят, что Z — общая подпоследовательность X и Y, если она является подпоследовательностью для X и Y. Тогда определим наибольшую общую подпоследовательность (НОП) двух данных последовательностей как общую подпоследовательность с максимальной длиной.

Теперь задача окончательно формализована, и можно приступать к ее решению. Введем понятие i-го префикса для заданной входной строки. Пусть имеется последовательность X = {x1, x2, …,xm}. Определим ее i-ый префикс (i = 1, 2, …, m) как Xi = {x1, x2, …, xi}, а X0 соответственно пустая последовательность. Для того, чтобы лучше понять структуру НОП, весьма полезна следующая теорема.

Теорема (о структуре НОП)

Пусть Z = {z1, z2, …, zk} — это любая НОП для последовательностей X = {x1, x2, …, xn} и Y = {y1, y2, …, ym}. Тогда

Если xn = ym, то zk = xn = ym и Zk−1 — это НОП Xn−1 и Ym−1. Если xn ≠ ym и zk ≠ xn, то Z — это НОП Xn−1 и Y.

Если xn ≠ ym и zk ≠ ym, то Z — это НОП X и Ym−1.

Непосредственно из теоремы следует способ нахождения НОП двух заданных последовательностей X = {x1, x2, …, xn} и Y = {y1, y2, …, ym}. Если xn = ym, то мы должны искать НОП для Xn−1 и Ym−1 и,

присоединив к этому результату xn = ym, мы получим НОП для X и Y. Если же xn ≠ ym, то мы должны решить две подзадачи: первая НОП Xn−1 и Y, вторая найти НОП X и Ym−1. Затем выбрать наибольший из двух результатов, это и будет НОП Xи Y. Заметим, что каждая из двух подзадач потребует решения вновь появившихся подзадач, которые в свою очередь будут выявлять все новые подзадачи.

Остается только выбрать какой из двух алгоритмов прогонки здесь больше подходит, прямой или обратной. С вычислительной точки зрения наиболее рациональным будет алгоритм прямой прогонки, вычисляющий НОП снизу вверх. В случае вычисления очень удобно организовать в виде таблицы c[0…n, 0…m], где c[i][j] соответствует длине НОП префиксов Xi и Yj. Значение c[i][j] можем вычислять по следующим правилам:

0, если i = 0 или j = 0;

c[i−1][j−1] + 1, если i, j > 0 и xi = yj; max(c[i][j−1], c[i−1][j]), если i, j > 0 и xi ≠ yj.

Таким образом, мы можем найти длину НОП, а для того чтобы найти саму НОП, необходимо хранить некоторую дополнительную информацию. Удобно хранить информацию, о том, по какому условию осуществляется переход. На примере это можно проиллюстрировать с помощью стрелочек, тогда, возвращаясь назад по стрелочкам, мы найдем все символы, которые входят в НОП.

30. Исчерпывающий перебор.

Исчерпывающий перебор

Многие важные задачи требуют поиска элемента со специальными свойствами в области, экспоненциально (или ещё быстрее) растущей с увеличением размера

экземпляра задачи. Обычно такие задачи возникают в случаях, когда присутствуют — явно или неявно

— комбинаторные объекты, такие как перестановки, сочетания или подмножества данного множества. Многие подобные задачи являются задачами оптимизации: в них требуется найти элемент, который максимизирует или минимизирует некоторые интересующие нас характеристики, как, например, длина

Соседние файлы в папке сиаод