Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Komb_1.doc
Скачиваний:
2
Добавлен:
16.09.2019
Размер:
458.24 Кб
Скачать

16

Н.Н.КЛЕВАНСКИЙ

КОМБИНАТОРНЫЕ ЗАДАЧИ В ПРОГРАММИРОВАНИИ

Конспект лекций

Саратов

1999 г.

1.АЛГОРИТМЫ НА ГРАФАХ

1.1.Машинное представление графов

Теорию графов начали разрабатывать для решения геометрических задач (рис1.1), в которых было несущественно, соединены ли точки конфигурации отрезками прямых или криволинейными дугами, какова длина линий и другие геометрические характеристики конфигурации. Важно лишь то, что каждая линия соединяет какие-либо две из заданных точек. Таким образом, можно дать определение графа как совокупности двух множеств V (точек) и E (линий), между элементами которых определено отношение инцидентности, причем каждый элемент инцидентен ровно двум элементам . Элементы множества V называются вершинами графа G, элементы множества E – его ребрами. В некоторых задачах инцидентные ребру вершины неравноправны и рассматриваются в определенном порядке. Тогда каждому ребру можно приписать направление от первой из инцидентных вершин ко второй. Граф в этом случае называется ориентированным по сравнению с ранее определенным неориентированным.

Первые исследования геометрических объектов с помощью графов принадлежат Эйлеру, оттуда же его формулы

F – A + S = 2 для тела F – A + S = 1 для замкнутой поверхности

В теории графов классическим способом представления графа служит матрица инциденций (табл.1.1). Это матрица А с n строками, соответствующими вершинам, и m столбцами, соответствующими ребрам. Для ориентированного графа столбец, соответствующий дуге , содержит –1 в строке, соответствующей вершине x, 1 – в строке, соответствующей вершине y, и 0 во всех остальных строках. В случае неориентированного графа столбец, соответствующий ребру содержит 1 в строках, соответствующих x и y, и нули в остальных строках.

Табл.1.1.Матрицы инциденций неориентированного и ориентированного графов (рис.1.1)

Неориентированный граф

{1,2}

{1,4}

{1,7}

{2,3}

{2,6}

{3,4}

{3,5}

{3,6}

{4,5}

{5,6}

{5,7}

{6,7}

1

1

1

1

0

0

0

0

0

0

0

0

0

2

1

0

0

1

1

0

0

0

0

0

0

0

3

0

0

0

1

0

1

1

1

0

0

0

0

4

0

1

0

0

0

1

0

0

1

0

0

0

5

0

0

0

0

0

0

1

0

1

1

1

0

6

0

0

0

0

1

0

0

1

0

1

0

1

7

0

0

1

0

0

0

0

0

0

0

1

1

Ориентированный граф

1

1

0

0

0

1

0

0

0

0

0

0

1

2

-1

-1

0

0

0

0

0

0

0

1

0

0

3

0

1

-1

-1

0

1

0

0

0

0

0

0

4

0

0

0

0

-1

-1

-1

0

0

0

0

0

5

0

0

1

0

0

0

1

-1

-1

0

0

0

6

0

0

0

1

0

0

0

1

0

-1

-1

0

7

0

0

0

0

0

0

0

0

1

0

1

-1

С алгоритмической точки зрения матрица инциденций является, вероятно, самым худшим способом представления графа, который только можно представить. Во-первых, он требует nm ячеек памяти, большинство из которых занято нулями. Неудобен также доступ к информации. Лучшим способом представления графа является матрица смежности (табл.1.2), определяемая как матрица В размера n n, в которой если существует ребро, идущее из вершины x в вершину y, и в противном случае. Для неориентированного графа важен факт наличия ребра, что приводит к симметричности матрицы смежности.

Табл.1.2.Матрицы смежности неориентированного и ориентированного графов (рис.1.1)

1

2

3

4

5

6

7

1

2

3

4

5

6

7

1

1

0

1

0

0

1

1

1

0

1

0

0

1

2

1

1

0

0

1

0

2

0

0

0

0

1

0

3

0

1

1

1

1

0

3

0

1

1

0

0

0

4

1

0

1

1

0

0

4

0

0

0

0

0

0

5

0

0

1

1

1

1

5

0

0

1

1

0

0

6

0

1

1

0

1

1

6

0

0

1

0

1

0

7

1

0

0

0

1

1

7

0

0

0

0

1

1

Основным преимуществом матрицы смежности является возможность за один шаг получить ответ на вопрос «существует ли ребро из x в y?» Недостаток – объем памяти n2 независимо от числа ребер. Более экономным в отношении памяти является метод представления графа с помощью списка пар вершин, соответствующих его ребрам. Для обоих графов (рис.1.1) такой список будет иметь вид.

Табл.1.3.Список ребер, соответствующий графам рис.1.1.

2 1

2 3

3 5

3 6

4 1

4 3

4 5

5 6

5 7

6 2

6 7

7 1

Объем памяти 2m. Неудобством является большое число шагов – порядка m в худшем случае, - необходимое для получения множества вершин, к которым ведут ребра из данной вершины. Ситуацию можно значительно улучшить, упорядочив множество пар лексикографически и применяя двоичный поиск, но лучшим решением во многих случаях оказывается структура данных следующего вида.

1: 2, 4, 7 1:

2: 1, 6, 3 2: 1, 3

3: 2, 6, 5, 4 3: 5, 6

а) 4: 1, 3, 5 б) 4: 1, 3, 5

5: 3, 6, 7, 4 5: 6, 7

6: 2, 7, 5, 3 6: 2, 7

7: 1, 5, 6 7: 1

Рис.1.2.Списки инцидентности неориентированного (а) и ориентированного (б) графов (рис.1.1).

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

Circ = data type [int] is create, element, change, delete, insert, size

Описание

Кольцевые списки circ - изменяемые однонаправленные кольцевые списки целых чисел. Операции delete и insert модифицируют списки. Кольцевой список имеет заголовочную часть, содержащую центральный элемент и ссылку на текущий элемент списка.

Операции

create = proc() returns(circ[int])

effects возвращает новый, пустой кольцевой список

element = proc(r:circ[int]) returns(int)

requires r создан и не пуст

effects возвращает текущий элемент кольцевого списка

change = proc(r:circ[int])

requires r создан и не пуст

effects переустанавливает ссылку заголовочной части на следующий элемент кольцевого списка r, который становится текущим

delete = proc(r:circ[int])

requires r создан и не пуст

modifies r

effects удаляет текущий элемент кольцевого списка r и переустанавливает ссылку заголовочной части на следующий после удаляемого элемент

insert = proc(r:circ[int], x:int)

requires r создан

modifies r

effects вставляет x в r перед текущим элементом и переустанавливает ссылку заголовочной части на элемент x

size = proc(r:сirc[int]) returns(int)

requires r создан

effects возвращает число элементов кольцевого списка r, если он пуст, то возвращает 0

end circ

Рис.1.3.Спецификация параметризованной абстракции данных для кольцевых списков.

Graph = data type [circ] is create, empty, fetch, store

Описание

Массивы graphмассивоподобные линейные списки кольцевых списков circ целых чисел

Операции

create = proc (n:int) returns (graph)

effects возвращает новый пустой массив

empty = proc (g:graph) returns (bool)

requires g существует

effects возвращает значение true, если массив g не содержит элементов, в противном случае – false

fetch = proc (g:graph; i:int) returns (circ) signals (bounds)

effects если i < 0 или i > n, то сигнализирует через bounds, иначе возвращает элемент g с индексом i

store = proc (g:graph; i:int; elem:circ) signals (bounds)

modifies g

effects если i < 0 или i > n, то сигнализирует через bounds, иначе присваиает elem значение элемента g с индексом i

end graph

Рис.1.4.Спецификация графа как «массива» кольцевых структур.

Графическая интерпретация введенных типов данных представлена на рис.1.5.

1.2.Поиск в глубину в графе

Существует много алгоритмов на графах, в основе которых лежит систематический перебор вершин графа, такой, что каждая вершина просматривается в точности один раз. Поэтому важной задачей является нахождение хороших методов поиска в графе. вообще говоря, метод поиска «хорош», если

  • он позволяет алгоритму решения задачи легко «погрузиться» в этот метод;

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

Одной из основных методик проектирования графовых алгоритмов является поиск в глубину (depth first search) в неориентированном графе. Общая идея этого метода состоит в следующем. Пусть поиск начинается с некоторой фиксированной вершины vo. Затем выбирается произвольная вершина u, смежная с vo, после чего процесс повторяется от u. В общем случае можно положить, что процесс подошел к вершине v. Если существует еще не просмотренная вершина u (u-v), то она рассматривается и, начиная с нее, продолжается поиск. Если же не существует ни одной не просмотренной вершины, смежной с v, то полагается, что вершина v использована, и осуществляется возврат в вершину, из которой попали в v, и процесс продолжается. Если v = vo, то поиск завершается.

Это может быть специфицировано следующей рекурсивной процедурой.

WG = proc (u:int; g:graph; НОВЫЙ: array of bool)

modifies НОВЫЙ

effects 1. “рассмотретьвершину u

2. НОВЫЙ[u] = false

3. c=graph.fetch(g,u)

4. i = circ.size (c)

5. пока i > 0

5.1. j = circ.element (c)

5.2. если НОВЫЙ[j], то WG(j),

иначе circ.change (c)

5.3 i = i-1

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

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

Depth = proc (g:graph)

effects 1. для i = 1 до n выполнить НОВЫЙ [i] = true

2. для i = 1 до n выполнить

если НОВЫЙ [i], то WG(i)

Рис.1.7.Спецификация процедуры поиска в глубину.

Полученный алгоритм просматривает каждую вершину в точности один раз и его сложность порядка O(n + m), то есть не более n + m. Следует также отметить, что алгоритм поиска в глубину в произвольном графе можно легко модифицировать так, чтобы он вычислял связные компоненты этого графа.

В связи с тем, что поиск в глубину играет важную роль в проектировании алгоритмов на графах, представим также нерекурсивную версию процедуры WG. Рекурсия устраняется стандартным способом при помощи стека (рис.1.8). Каждая просмотренная вершина помещается в стек и удаляется из стека после использования (рис.1.9).

Stack = data type [t:type] is new, is_empty, top, pop, push

Описание

Тип данных stack используется для хранения элементов t. Элементы могут извлекаться из стека или добавляться в него только по методу LIFO (Last Input - First Output)

Операции

function new:stack[t]

effects возвращает новый стек, без элементов в нем

function is_empty(s:stack[t]):boolean

effects возвращает значение true, если в стеке s нет элементов, в противном случае возвращает значение false

function top(s:stack[t]):t

requires s создан и не пуст

effects возвращает значение начального элемента стека s

pop = proc(s:stack[t], e:t)

requires s создан и не пуст

modifies s

effects добавляет элемент e в начало стека s

function push(s:stack[t]):t

requires s создан и не пуст

modifies s

effects возвращает значение элемента из начала стека s и удаляет этот элемент из стека

end stack

Рис.1.8.Спецификация параметризованной абстракции данных для стека.

WG1 = proc (u:int; g:graph; НОВЫЙ: array of bool)

modifies НОВЫЙ

effects 1. s = stack.new( ); stack.pop(s,u); «рассмотреть» вершину u; НОВЫЙ[u] = false

2. пока (not stack.is_empty(s)) выполнять

2.1. t = stack.top(s); c = graph.fetch(g,t)

2.2. если circ.size(c) = 0, то b = false иначе b = not НОВЫЙ[circ.element(c)]

2.3. пока b выполнять

2.3.1. circ.change(c); t1 = circ.element; c = graph.fetch(g,t1)

2.3.2. если circ.size(c) = 0, то b = false иначе b = not НОВЫЙ[circ.element(c)]

2.4. если circ.size(c)<>0, то

2.4.1. t = circ.element(c)

2.4.2. stack.pop(s,t)

2.4.3. НОВЫЙ[t] = false

иначе t = stack.push(s)

Рис.1.9.Процедурная спецификация нерекурсивной процедуры для поиска в глубину.

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]