угольники - это источники, соответствующие им; указаны начальные
и конечные вершины. На новых стрелках (их 6) букв не написано.
Конкатенации соответствует картинка
|--------| |--------| |--------|
Н*--->|*Н1 К1*|---->----|*Н2 К2*| ---->----|*Н3 К3*|-->--*К
|--------| |--------| |--------|
Наконец, итерации соответствует картинка
Н*--------->----------*----------->----------*К
/ \
/ \
| |
V ^
| |
-------------
| *Н1 К1* |
-------------
10.7.10. Дан источник. Построить конечный автомат, проверя-
ющий, принадлежит ли входное слово множеству, соответствующему
источнику (т. е. можно ли прочесть это слово, идя из Н в К).
Решение. Состояниями автомата будут множества вершин источ-
ника. Именно, прочтя некоторое начало X входного слова, мы будем
помнить множество всех вершин источника, в которые можно пройти
из начальной, прочитав на пути слово X.
Оказывается, что регулярные выражения, автоматы и источники
распознают одни и те же множества. Чтобы убедиться в этом, нам
осталось решить такую задачу:
10.7.11. Дан источник. Построить регулярное выражение, за-
дающее то же множество, что и этот источник.
Решение. (Сообщено участниками просеминара по логике.)
Пусть источник имеет вершины 1..k. Будем считать, что 1 - это
начало, а k - конец. Через D[i, j, s] обозначим множество всех
слов, которые можно прочесть на пути из i в j, если в качестве
промежуточных пунктов разрешается использовать только вершины
1,...,s. Согласно определению, источнику соответствует множество
D[1,k, k].
Индукцией по s будем доказывать регулярность всех множеств
D[i, j,s] при всех i и j. При s=0 это очевидно (промежуточные
вершины запрещены, поэтому каждое из множеств состоит только из
букв).
Из чего состоит множество D[i, j,s+1]? Отметим на пути мо-
менты, в которых он заходит в s+1-ую вершину. При этом путь раз-
бивается на части, каждая из которых уже не заходит в нее. По-
этому легко сообразить, что
D[i, j,s+1] = (D[i, j,s]| (D[i, s+1,s] D[s+1,s+1,s]* D[s+1,j, s]))
(вольность записи: мы используем для операций над множествами
обозначения как для регулярных выражений). Остается воспользо-
ваться предположением индукции.
10.7.12. Где еще используется то же самое рассуждение?
Ответ. В алгоритме Флойда вычисления цены кратчайшего пути,
см. главу 9 (Некоторые алгоритмы на графах).
10.7.13. Доказать, что класс множеств, задаваемых регуляр-
ными выражениями, не изменился бы, если бы мы разрешили ис-
пользовать не только объединение, но и отрицание (а следова-
тельно, и пересечение - оно выражается через объединение и отри-
цание).
Решение. Для автоматов переход к отрицанию очевиден.
Замечание. На практике важную роль играет число состояний
автомата. Оказывается, что тут все не так просто, и переход о
источника к автомату требует экспоненциального роста числа сос-
тояний. Подробное рассмотрение связанных с этим теоретических и
практических вопросов - дело особое.
Глава 11. Представление множеств. Хеширование.
11.1. Хеширование с открытой адресацией
В предыдущей главе было несколько представлений для мно-
жеств, элементами которых являются целые числа произвольной ве-
личины. Однако в любом из них хотя бы одна из операций проверки
принадлежности, добавления и удаления элемента требовала коли-
чества действий, пропорционального числу элементов множества. На
практике это бывает слишком много. Существуют способы, позволя-
ющие получить для всех трех упомянутых операций оценку C*log n.
Один из таких способов мы рассмотрим в следующей главе. В этой
главе мы разберем способ, которые хотя и приводит к C*n действи-
ям в худшем случае, но зато "в среднем" требует значительно
меньшего их числа. (Мы не будем уточнять слов "в среднем", хотя
это и можно сделать.) Этот способ называется хешированием.
Пусть нам необходимо представлять множества элементов типа
T, причем число элементов заведомо меньше n. Выберем некоторую
функцию h, определенную на значениях типа T и принимающую значе-
ния 0..(n-1). Было бы хорошо, чтобы эта функция принимала на
элементах будущего множества по возможности более разнообразные
значения. Худший случай - это когда ее значения на всех элемен-
тах хранимого множества одинаковы. Эту функцию будем называть
хеш-функцией.
Введем два массива
val: array [0..n-1] of T;
used: array [0..n-1] of boolean;
(мы позволяем себе писать n-1 в качестве границы в определении
типа, хотя в паскале это не разрешается). В этих массивах будут
храниться элементы множества: оно равно множеству всех val [i]
для тех i, для которых used [i], причем все эти val [i] различ-
ны. По возможности мы будем хранить элемент t на месте h(t),
считая это место "исконным" для элемента t. Однако может слу-
читься так, что новый элемент, который мы хотим добавить, пре-
тендует на уже занятое место (для которого used истинно). В этом
случае мы отыщем ближайшее справа свободное место и запишем эле-
мент туда. ("Справа" значит "в сторону увеличения индексов";
дойдя до края, мы перескакиваем в начало.) По предположению,
число элементов всегда меньше n, так что пустые места гарантиро-
ванно будут.
Формально говоря, в любой момент должно соблюдаться такое
требование: для любого элемента множества участок справа от его
исконного места до его фактического места полностью заполнен.
Благодаря этому проверка принадлежности заданного элемента
t осуществляется легко: встав на h(t), двигаемся направо, пока
не дойдем до пустого места или до элемента t. В первом случае
элемент t отсутствует в множестве, во втором присутствует. Если
элемент отсутствует, то его можно добавить на найденное пустое
место. Если присутствует, то можно его удалить (положив used =
false).
11.1.1. В предыдущем абзаце есть ошибка. Найдите ее и
исправьте.
Решение. Дело в том, что при удалении требуемое свойство
"отсутствия пустот" может нарушиться. Поэтому будем делать так.
Создав дыру, будем двигаться направо, пока не натолкнемся на еще
одно пустое место (тогда на этом можно успокоиться) или на эле-
мент, стоящий не на исконном месте. Во втором случае посмотрим,
не нужно ли этот элемент поставить на место дыры. Если нет, то
продолжаем поиск, если да, то затыкаем им старую дыру. При этом
образуется новая дыра, с которой делаем все то же самое.
11.1.2. Написать программы проверки принадлежности, добав-
ления и удаления.
Решение.
function принадлежит (t: T): boolean;
| var i: integer;
begin
| i := h (t);
| while used [i] and (val [i] <> t) do begin
| | i := (i + 1) mod n;
| end; {not used [i] or (val [i] = t)}
| belong := used [i] and (val [i] = t);
end;
procedure добавить (t: T);
| var i: integer;
begin
| i := h (t);
| while used [i] and (val [i] <> t) do begin
| | i := (i + 1) mod n;
| end; {not used [i] or (val [i] = t)}
| if not used [i] then begin
| | used [i] := true;
| | val [i] := t;
| end;
end;
procedure исключить (t: T);
| var i, gap: integer;
begin
| i := h (t);
| while used [i] and (val [i] <> t) do begin
| | i := (i + 1) mod n;
| end; {not used [i] or (val [i] = t)}
| if used [i] and (val [i] = t) then begin
| | used [i] := false;
| | gap := i;
| | i := (i + 1) mod n;
| | while used [i] do begin
| | | if i = h (val[i]) then begin
| | | | i := (i + 1) mod n;
| | | end else if dist(h(val[i]),i) < dist(gap, i) then begin
| | | | i := (i + 1) mod n;
| | | end else begin
| | | | used [gap] := true;
| | | | val [gap] := val [i];
| | | | used [i] := false;
| | | | gap := i;
| | | | i := i + 1;
| | | end;
| | end;
| end;
end;
Здесь dist (a, b) - измеренное по часовой стрелке (слева
направо) расстояние от a до b, т. е.
dist (a, b) = (b - a + n) mod n.
(Мы прибавили n, так как функция mod правильно работает только
при положительном делимом.)
11.1.3. Существует много вариантов хеширования. Один из них
таков: обнаружив, что исконное место (обозначим его i) занято,
будем искать свободное не среди i+1, i+2,..., а среди r(i),
r(r(i)), r(r(r(i))),..., где r - некоторое отображение 0..n-1 в
себя. Какие при этом будут трудности?
Ответ. (1) Не гарантируется, что если пустые места есть, то
мы их найдем. (2) При удалении неясно, как заполнять дыры. (На
практике во многих случаях удаление не нужно, так что такой спо-
соб также применяется. Считается, что удачный подбор r может
предотвратить образование "скоплений" занятых ячеек.)
11.1.4. Пусть для хранения множества всех правильных
русских слов в программе орфографии используется хеширование.
Что нужно добавить, чтобы к тому же уметь находить английский
перевод любого правильного слова?
Решение. Помимо массива val, элементы которого являются
русскими словами, нужен параллельный массив их английских пере-
водов.
11.2. Хеширование со списками
На хеш-функцию с m значениями можно смотреть как на способ
свести вопрос о хранении одного большого множества к вопросу о
хранении нескольких меньшим. Именно, если у нас есть хеш-функция
с m значениями, то любое множество разбивается на m подмножеств
(возможно, пустых), соответствующих возможных значениям
хэш-функции. Вопрос о проверке принадлежности, добавлении или
удалении для большого множества сводится к такому же вопросу для
одного из меньших (чтобы узнать, для какого, надо посмотреть на
значение хеш-функции).
В принципе, эти меньшие множества могут храниться любым
способом (раз они малы, это не очень важно), но удобно их хра-
нить с помощью ссылок, поскольку нам известен их суммарный раз-
мер (равный числу элементов хешируемого множества). Следующая
задача предлагает реализовать этот план.
11.2.1. Пусть хеш-функция принимает значения 1..k. Для каж-
дого значения хеш-функции рассмотрим список всех элементов мно-
жества с данным значением хеш-функции. Будем хранить эти k спис-
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |


