ЛЕКЦИЯ №5
Элементы теории графов: основные понятия и определения. Способы представления графов и деревьев. Примеры применения деревьев в программировании. Примеры алгоритмов обработки деревьев и графов. Сравнение рекурсивных и итеративных алгоритмов, решающих некоторые классические задачи теории графов.
Чуть-чуть истории
Теория графов - довольно молодая наука (по сравнению, скажем, с геометрией). В 1736 году Санкт-Петербургская академия наук опубликовала труд Леонарда Эйлера, где рассматривалась задача о кенигсбергских1) мостах ("Можно ли, пройдя все городские мосты ровно по одному разу, вернуться в исходную точку?"). Это была первая работа по будущей теории графов.
Особенно приятно то, что в данном случае Россия - родина пусть и не новой породы слонов, но зато нового научного направления!
Графы: определения и примеры
Итак, перейдем к изложению некоторых понятий современной теории графов.
Неориентированные графы
Граф - это двойка <V, E>, где V - непустое множество вершин, а Е - множество ребер, соединяющих эти вершины попарно2). Две вершины, связанные между собой ребром, равноправны, и именно поэтому такие графы называются неориентированными: нет никакой разницы между "началом" и "концом" ребра.
Говоря простым языком, граф - это множество точек (для удобства изображения - на плоскости) и попарно соединяющих их линий (не обязательно прямых). В графе важен только факт наличия связи между двумя вершинами. От способа изображения этой связи структура графа не зависит.
Например, три графа на рис. 1 совпадают, а два графа на рис. 2 - различны.
Рис. 1. Три способа изображения одного графа
Из приведенного выше определения вытекает, что в графах не бывает петель - ребер, соединяющих некоторую вершину саму с собой. Кроме того, в классическом графе не бывает двух различных ребер, соединяющих одну и ту же пару вершин.
Ребро е и вершина v называются инцидентными друг другу, если вершина v является одним из концов ребра е.
Рис. 2. Пример двух разных графов
Рис. 3. Псевдограф
Любому ребру инцидентно ровно две вершины, а вот вершине может быть инцидентно произвольное количество ребер, это количество и определяет степень вершины. Изолированная вершина вообще не имеет инцидентных ей ребер (ее степень равна 0).
Две вершины называются смежными, если они являются разными концами одного ребра (иными словами, эти вершины инцидентны одному ребру). Аналогично, два ребра называются смежными, если они инцидентны одной вершине.
Путь в графе - это последовательность вершин (без повторений), в которой любые две соседние вершины смежны. Например, в графе, изображенном на рис. 1, есть два различных пути из вершины a в вершину с: adbc и abc.
Вершина v достижима из вершины u, если существует путь, начинающийся в u и заканчивающийся в v.
Граф называется связным, если все его вершины взаимно достижимы.
Компонента связности - это максимальный связный подграф. В общем случае граф может состоять из произвольного количества компонент связности. Заметим, что любая изолированная вершина является отдельной компонентой связности. На рис. 4 изображен граф, состоящий из четырех компонент связности: [abhk], [gd], [c] и [f].
Длина пути - количество ребер, из которых этот путь состоит. Например, длина уже упомянутых путей adbc и abc - 3 и 2 соответственно.
Рис. 4. Несвязный граф
Говорят, что вершина v принадлежит k-му уровню относительно вершины u, если существует путь из u в v длиной ровно k ребер. Одна и та же вершина может относиться к разным уровням. Например, в графе, изображенном на рис. 1, относительно вершины a существует 4 уровня:
- 0) a;
- 1) b, d;
- 2) b, d, c (пути adb, abd, abc);
- 3) c (путь adbc).
Расстояние между вершинами u и v - это длина кратчайшего пути от u до v. Из этого определения видно, что расстояние между вершинами a и c в графе на рис. 1 равно 2.
Цикл - это замкнутый путь. Все вершины в цикле, кроме первой и последней, должны быть различны. Например, циклом является путь abda в графе на рис. 1.
Эйлеров граф - это граф, в котором существует путь или цикл, содержащий все ребра графа (вершины могут повторяться). Именно такие графы положительно решают упомянутую в начале лекции задачу о кенигсбергских мостах. Например, граф на рис. 5 является Эйлеровым: искомым путем в нем будет dbacfbcd.
Рис. 5. Граф Эйлера
Гамильтонов граф - это граф, в котором существует путь или цикл (без повторений), содержащий все вершины графа (см. рис. 5; искомый цикл: abdfca).
Ориентированные графы
Орграф - это граф, все ребра которого имеют направление. Такие направленные ребра называются дугами. На рисунках дуги изображаются стрелочками (см. рис. 6).
Рис. 6. Орграф
В отличие от ребер, дуги соединяют две неравноправные вершины: одна из них называется началом дуги (дуга из нее исходит), вторая - концом дуги (дуга в нее входит). Можно сказать, что любое ребро - это пара дуг, направленных навстречу друг другу.
Если в графе присутствуют и ребра, и дуги, то его называют смешанным.
Все основные понятия, определенные для неориентированных графов (инцидентность, смежность, достижимость, длина пути и т.п.), остаются в силе и для орграфов - нужно лишь заменить слово "ребро" словом "дуга". А немногие исключения связаны с различиями между ребрами и дугами.
Степень вершины в орграфе - это не одно число, а пара чисел: первое характеризует количество исходящих из вершины дуг, а второе - количество входящих дуг.
Путь в орграфе - это последовательность вершин (без повторений), в которой любые две соседние вершины смежны, причем каждая вершина является одновременно концом одной дуги и началом следующей дуги. Например, в орграфе на рис. 6 нет пути, ведущего из вершины 2 в вершину 5. "Двигаться" по орграфу можно только в направлениях, заданных стрелками.
| Орграф | Вершины | Дуги |
|---|---|---|
| Чайнворд | Слова | Совпадение последней и первой букв (возможность связать два слова в цепочку) |
| Стройка | Работы | Необходимое предшествование (например, стены нужно построить раньше, чем крышу, т. п.) |
| Обучение | Курсы | Необходимое предшествование (например, курс по языку Pascal полезно изучить прежде, чем курс по Delphi, и т.п.) |
| Одевание ребенка | Предметы гардероба | Необходимое предшествование (например, носки должны быть надеты раньше, чем ботинки, и т.п.) |
| Европейский город | Перекрестки | Узкие улицы с односторонним движением |
| Организация | Сотрудники | Иерархия (начальник - подчиненный) |
Взвешенные графы
Взвешенный (другое название: размеченный) граф (или орграф) - это граф (орграф), некоторым элементам которого (вершинам, ребрам или дугам) сопоставлены числа. Наиболее часто встречаются графы с помеченными ребрами. Числа-пометки носят различные названия: вес, длина, стоимость.
Замечание: Обычный (не взвешенный) граф можно интерпретировать как взвешенный, все ребра которого имеют одинаковый вес 1.
Длина пути во взвешенном (связном) графе - это сумма длин (весов) тех ребер, из которых состоит путь. Расстояние между вершинами - это, как и прежде, длина кратчайшего пути. Например, расстояние от вершины a до вершины d во взвешенном графе, изображенном на рис. 7, равно 6.
Рис. 7. Взвешенный граф
N-периферия вершины v - это множество вершин, расстояние до каждой из которых (от вершины v) не меньше, чем N.
| Граф | Вершины | Вес вершины | Ребра (дуги) | Вес ребра (дуги) |
|---|---|---|---|---|
| Таможни | Государства | Площадь территории | Наличие наземной границы | Стоимость получения визы |
| Переезды | Города | Стоимость ночевки в гостинице | Дороги | Длина дороги |
| Супер-чайнворд | Слова | - | Совпадение конца и начала слов(возможность "сцепить" слова) | Длина пересекающихся частей |
| Карта | Государства | Цвет на карте | Наличие общей границы | - |
| Сеть | Компьютеры | - | Сетевой кабель | Стоимость кабеля |
Способы представления графов
Существует довольно большое число разнообразных способов представления графов. Однако мы изложим здесь только самые полезные с точки зрения программирования.
Матрица смежности
Матрица смежности Sm - это квадратная матрица размером NxN (N - количество вершин в графе), заполненная единицами и нулями по следующему правилу:
Если в графе имеется ребро e, соединяющее вершины u и v, то Sm[u,v] = 1, в противном случае Sm[u,v] = 0.
Заметим, что данное определение подходит как ориентированным, так и неориентированным графам: матрица смежности для неориентированного графа будет симметричной относительно своей главной диагонали, а для орграфа - несимметричной.
Задать взвешенный граф при помощи матрицы смежности тоже возможно. Необходимо лишь внести небольшое изменение в определение:
Если в графе имеется ребро e, соединяющее вершины u и v, то Sm[u,v] = ves(e), в противном случае Sm[u,v] = 0.
Это хорошо согласуется с замечанием, сделанным в предыдущем пункте: невзвешенный граф можно интерпретировать как взвешенный, все ребра которого имеют одинаковый вес 1.
Небольшое затруднение возникнет в том случае, если в графе разрешаются ребра с весом 0. Тогда придется хранить два массива: один с нулями и единицами, которые служат показателем наличия ребер, а второй - с весами этих ребер.
В качестве примера приведем матрицы смежности для трех графов, изображенных на рис. 5, рис. 6 и рис. 7 (см. рис. 8).
| a | b | c | d | f | 1 | 2 | 3 | 4 | 5 | a | b | c | d | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | a | 0 | 1 | 10 | 0 |
| b | 1 | 0 | 1 | 1 | 1 | 2 | 0 | 0 | 0 | 0 | 0 | b | 1 | 0 | 2 | 10 |
| c | 1 | 1 | 0 | 1 | 1 | 3 | 1 | 1 | 0 | 0 | 1 | c | 10 | 2 | 0 | 3 |
| d | 0 | 1 | 1 | 0 | 1 | 4 | 0 | 0 | 1 | 0 | 0 | d | 0 | 10 | 3 | 0 |
| f | 0 | 1 | 1 | 1 | 0 | 5 | 0 | 0 | 0 | 0 | 0 |
Удобство матрицы смежности состоит в наглядности и прозрачности алгоритмов, основанных на ее использовании. А неудобство - в несколько завышенном требовании к памяти: если граф далек от полного, то в массиве, хранящем матрицу смежности, оказывается много "пустых мест" (нулей). Кроме того, для "общения" с пользователем этот способ представления графов не слишком удобен: его лучше применять только для внутреннего представления данных.
Список ребер
Этот способ задания графов наиболее удобен для внешнего представления входных данных. Пусть каждая строка входного файла содержит информацию об одном ребре (дуге):
<номер_начальной_вершины> <номер_конечной_вершины> [<вес_ребра>]
В качестве примера приведем списки ребер (дуг), задающие те же три графа с рис. 5, рис. 6 и рис. 7 (см. рис. 9).
Если задается ориентированный граф, то номера вершин понимаются как упорядоченная пара, а если граф неориентированный - как неупорядоченная.
Списки смежности
Этот способ задания графов подразумевает, что для каждой вершины будет указан список всех смежных с нею вершин (для орграфа - список вершин, являющихся концами исходящих дуг). Конкретный формат входного файла, содержащего списки смежности, необходимо обговорить отдельно. Например, в нашем случае начальная вершина отделена от списка смежности двоеточием:
<номер_начальной_вершины>: <номера_смежных_вершин>
Наиболее естественно применять этот способ для задания орграфов, однако и для остальных вариантов он тоже подходит.
В качестве примера приведем списки смежности, задающие все те же три графа, изображенные на рис. 5, рис. 6 и рис. 7 (см. рис. 10).
Иерархический список
Собственно, этот способ представления графов является всего лишь внутренней реализацией списка смежности: в одном линейном списке содержатся номера "начальных вершин", а в остальных - номера смежных вершин или указатели на эти вершины. В качестве примера (см. рис. 11) приведем иерархический список, задающий орграф, изображенный на рис. 6.
Рис. 11. Пример иерархического списка
type uk_versh = ^vershina;
uk_duga = ^duga
vershina = record number : integer;
sled_vershina : uk_versh;
spisok_dug : uk_duga
end;
duga = record konec_dugi : uk_versh;
sled_duga : uk_duga;
end;
Очевидное преимущество такого способа представления графов заключается в экономичном использовании памяти. И даже небольшая избыточность данных, к которой приходится прибегать в случае неориентированного графа, задавая каждое ребро как две дуги, искупается гибкостью всей структуры, что особенно удобно при необходимости частых перестроений в процессе работы программы.
Если в приведенные описания типов данных добавить поля, которые могли бы хранить веса вершин и дуг, то таким же способом можно задавать и взвешенные графы (орграфы).
Деревья
Дерево - это частный случай графа, наиболее широко применяемый в программировании.
Основные определения
Существует довольно много равносильных определений деревьев, вот лишь некоторые из них.
- Дерево - это связный граф без циклов.
- Дерево - это связный граф, в котором при N вершинах всегда ровно N-1 ребро.
- Дерево - это граф, между любыми двумя вершинами которого существует ровно один путь.
Аналогичным образом определяется и ориентированное дерево - как орграф, в котором между любыми двумя вершинами существует не более одного пути.
Рис. 12. Корневое дерево высоты 3
Мы будем изучать и использовать только один частный случай ориентированных деревьев - корневые деревья (см. рис. 12).
Корневое дерево - это ориентированное дерево, в котором можно выделить вершины трех видов: корень, листья (другое их название: терминальные вершины) и остальные вершины (нетерминальные); причем должны выполняться два обязательных условия:
- из листьев не выходит ни одна дуга; из других вершин может выходить сколько угодно дуг;
- в корень не заходит ни одна дуга; во все остальные вершины заходит ровно по одной дуге.
Традиционно в математике и в родственных ей науках (в том числе и в теоретическом программировании) деревья "растут" вниз головой: это делается просто для удобства наращивания листьев в случае необходимости. Таким образом, на рисунках корень дерева оказывается самой верхней вершиной, а листья - самыми нижними.
Предок вершины v - это вершина, из которой исходит дуга, заходящая в вершину v. Потомок вершины v - это вершина, в которую заходит дуга, исходящая из вершины v. В этих терминах можно дать другие определения понятиям корень и лист: у корня нет предков, у листа нет потомков.
Бинарное дерево - это корневое дерево, каждая вершина которого имеет не более двух потомков. В таком случае иногда говорят о левом потомке и правом потомке для текущей вершины.
Высота корневого дерева - это максимальное количество дуг, отделяющих листья от корня. Если дерево не взвешенное, то его высота - это просто расстояние от корня до самого удаленного листа.
И в заключение мы приведем определение, связывающее произвольные графы с деревьями более плотно.
Каркас графа - это дерево, полученное после выбрасывания из графа некоторых ребер (см. рис. 13).
Рис. 13. Каркас графа
Примером каркаса является (корневое) дерево кратчайших путей от некоторой выделенной вершины (она будет корнем каркаса) до всех остальных вершин графа.
Способы представления деревьев
Поскольку любое дерево является графом, то его можно задавать любым из способов, перечисленных в п. "Способы представления графов". Однако существуют и специальные способы представления, предназначенные только для деревьев. Мы рассмотрим только два наиболее распространенных частных случая.
Представление корневого дерева
Этот способ подходит только для тех корневых деревьев, у которых точно известно максимальное количество потомков для любой вершины.
type ukazatel = ^tree;
tree = record znachenie : integer;
siblings : array[1..S] of ukazatel;
end;
Разумеется, в общем случае значение переменной S (количество потомков) может достигать N-1 (N - количество всех вершин в дереве). Однако ясно, что в такой ситуации особого смысла в динамической древесной структуре нет: экономии памяти не получается. Гораздо лучше, если у всех вершин примерно одинаковое и заранее известное количество потомков.
Представление бинарного дерева
Разновидностью описанного выше частного случая является бинарное корневое дерево: каждая его вершина имеет не более двух потомков:
type ukazatel = ^tree;
tree = record znachenie : integer;
left_sibling : ukazatel;
right_sibling: ukazatel;
end;
Примеры использования деревьев
Здесь мы ограничимся только примерами использования бинарных корневых деревьев: именно такой вид графа чаще всего применяется в программировании.
Дерево двоичного поиска
Дерево двоичного поиска для множества чисел S - это размеченное бинарное дерево, каждой вершине которого сопоставлено число из множества S, причем все пометки удовлетворяют следующим условиям:
- существует ровно одна вершина, помеченная любым числом из множества S;
- все пометки левого поддерева строго меньше, чем пометка текущей вершины;
- все пометки правого поддерева строго больше, чем пометка текущей вершины.
Если выражаться простым языком, то структура дерева двоичного поиска подчиняется простому правилу: "если больше - направо, если меньше - налево".
Например, для набора чисел 7, 3, 5, 2, 8, 1, 6, 10, 9, 4, 11 получится такое дерево (см. рис. 11.14).
Для того чтобы правильно учесть повторения чисел, можно ввести дополнительное поле, которое будет хранить количество вхождений для каждого числа.
Более подробно процессы построения и анализа дерева бинарного поиска будут изложены в следующей лекции, посвященной алгоритмам, использующим деревья и графы.
Рис. 14. Дерево двоичного поиска
Дерево частотного словаря
Дерево частотного словаря - это результат построения дерева двоичного поиска не для чисел, а для слов некоторого текста. Генерирование дерева частотного словаря полезно при подсчете количества вхождений каждого слова в некоторый текст.
Приведем описание структуры этого дерева:
type ukazatel = ^tree;
derevo = record slovo : string[20];
kolichestvo : integer;
levyj : ukazatel;
pravyj: ukazatel;
end;
Дерево синтаксического анализа
Дерево синтаксического анализа арифметического выражения - это бинарное дерево, листьями которого служат операнды, а остальными вершинами - операции, причем уровень вершины соответствует приоритету выполнения операции: чем ближе к листьям, тем приоритет выше.
Например, на рис.15 изображено дерево синтаксического анализа для выражения ((a / (b + c)) + (x * (y - z))).
Деревья синтаксического разбора строятся компиляторами во время синтаксического анализа программ. Помимо арифметических выражений, которые являются простейшим случаем, аналогичные, но более сложные деревья строятся для всех грамматических конструкций компилируемой программы.
Рис. 15. Дерево синтаксического анализа
1) Ныне - Калининград.
2) Более корректно: Е - это множество двухэлементных подмножеств (неупорядоченных пар) множества V, называемых ребрами. Из такого определения видно, что любые две вершины могут быть соединены не более чем одним ребром.
Генерация дерева синтаксического анализа
Одно и то же арифметическое выражение может быть записано тремя способами:
-
Инфиксный способ записи (знак операции находится между операндами):
((a / (b + c)) + (x * (y - z)))
Все арифметические операции, привычные нам со школьных лет, записываются именно таким образом.
-
Префиксный способ записи (знак операции находится перед операндами):
+( /(a, +(b,c)), *(x, -(y,z)))
Из знакомых всем нам функций префиксный способ записи используется, например, для sin(x), tg(x), f(x,y,z) и т.п.
-
Постфиксный способ записи (знак операции находится после операндов):
((a,(b,c)+ )/ ,(x,(y,z)- )* )+
Этот способ записи менее распространен, однако и с ним многим из нас приходилось сталкиваться уже в школе: примером будет n! (факториал).
Разумеется, вид дерева синтаксического анализа (ДСА) арифметического выражения не зависит от способа записи этого выражения (см. рис. 1), поскольку определяет его не форма записи, а порядок выполнения операций. Но процесс построения дерева, конечно же, зависит от способа записи выражения. Далее мы разберем все три варианта алгоритма построения ДСА.
Рис. 1. Дерево синтаксического анализа и способ описания его элементов
type ukaz = ^tree;
tree = record
symbol: char;
left: ukaz;
right: ukaz;
end;
Построение из инфиксной записи
Для простоты мы будем считать, что правильное арифметическое выражение подается в одной строке, без пробелов, а каждый операнд записан одной буквой. Приоритет операций определяется расставленными скобками: ими должна быть снабжена каждая операция.
Алгоритм Infix
Если не достигнут конец строки ввода, прочитать очередной символ.
Если этот символ - открывающая скобка, то:
- создать новую вершину дерева;
- вызвать алгоритм Infix для ее левого поддерева;
- прочитать символ операции и занести его в текущую вершину;
- вызвать алгоритм Infix для правого поддерева;
- прочитать символ закрывающей скобки (и ничего с ним не делать).
Иначе:
- создать новую вершину дерева;
- занести в нее этот символ.
Реализация
Мы воспользуемся здесь описанием типа данных ukaz, приведенным на рис. 12.1:
procedure infix(var p: ukaz);
var c: char;
begin
read(c);
if c = '('
then begin
new(p);
infix(p^.left);
read(p^.symbol); {'+', '-', '*', '/'}
infix(p^.right);
read(c); {')'}
end
else begin {'a'..'z','A'..'Z'}
new(p);
p^.symbol:= c;
p^.right:= nil;
p^.left:= nil
end;
end;
begin
...
infix(root);
...
end.
Построение из префиксной записи
Для простоты предположим, что правильное арифметическое выражение подается в одной строке, без пробелов, а каждый операнд записан одной буквой. Кроме того, будем считать, что из записи удалены все скобки: это вполне допустимо, так как операция всегда предшествует своим операндам, следовательно, путаница в порядке выполнения невозможна.
Алгоритм Prefix
- Если не достигнут конец строки ввода, прочитать очередной символ.
- Создать новую вершину дерева, записать в нее этот символ.
- Если символ - операция, то:
- вызвать алгоритм Prefix для левого поддерева;
- вызвать алгоритм Prefix для правого поддерева.
Реализация
Вновь воспользуемся описанием типа данных ukaz, приведенным на рис. 12.1:
procedure prefix(var p: ukaz);
begin
new(p);
read(p^.symbol);
if p^.symbol in ['+','-','*','/']
then begin prefix(p^.left);
prefix(p^.right);
end
else begin p^.left:= nil;
p^.right:= nil;
end
end;
begin
...
prefix(root);
...
end.
Построение из постфиксной записи
Для простоты предположим, что правильное арифметическое выражение подается в одной строке, без пробелов, а каждый операнд записан одной буквой. Кроме того, снова будем считать, что из записи удалены все скобки.
Алгоритм Postfix
- Если не достигнут конец строки ввода, прочитать очередной символ,если этот символ - операнд, то занести его в стек2),иначе (символ - операция):
- создать новый элемент, записать в него эту операцию;
- достать из стека два верхних (последних) элемента, присоединить их в качестве левого и правого операндов в новый элемент;
- занести полученный "треугольник" в стек.
По окончании работы этого алгоритма в стеке будет содержаться ровно один элемент - указатель на корень построенного дерева.
Реализация
Для того чтобы упростить работу, добавим в структуру элемента дерева (см. рис. 1.12) дополнительное поле next:ukaz, которое будет служить для связки стека:
stek:= nil;
while not eof(f) do
begin
new(p);
read(f,p^.symbol);
if p^.symbol in ['+','-','*','/']
then begin
p^.right:= stek;
p^.left:= stek^.next;
p^.next:= stek^.next^.next;
stek:= p
end
else begin
p^.left:= nil;
p^.right:= nil;
p^.next:= stek;
stek:= p
end;
end;
Обходы деревьев и графов
Прежде чем приступить к изложению алгоритмов обхода, дадим пару необходимых определений.
Обход дерева - это некоторая последовательность посещения всех его вершин.
Обход графа - это обход некоторого его каркаса.
В этом разделе будут представлены только алгоритмы обхода бинарных деревьев. Большинство из них может быть с легкостью изменено для случая произвольного корневого дерева, каковым является и каркас произвольного графа.
Напомним, что структуру бинарного дерева мы описываем следующим образом:
type ukazatel = ^tree;
tree = record mark: integer;
left: ukazatel;
right: ukazatel;
end;
Итак, приступим теперь к изучению различных вариантов обхода деревьев и графов.
Прямой обход
Другие названия
Префиксный обход: результатом прямого обхода3) дерева синтаксического анализа арифметического выражения будет префиксный вариант записи этого выражения.
Обход в глубину "сверху вниз": название имеет смысл лишь в случае стандартного расположения дерева корнем кверху.
Алгоритм PreOrder
- Начать с корня дерева.
- Пометить текущую вершину.
- Совершить прямой обход левого поддерева.
- Совершить прямой обход правого поддерева.
Замечание: Этот алгоритм может быть естественным образом распространен и на случай произвольного корневого дерева.
Реализация
procedure preorder(p:ukaz; k:integer);
begin p^.mark:= k;
if p^.left<>nil then preorder(p^.left,k+1);
if p^.right<>nil then preorder(p^.right,k+1);
end;
begin
...
preorder(root,1); {Вызов из тела программы}
... end.
Рис. 2. Последовательность нумерации вершин при прямом обходе дерева
Прямой обход произвольного связного графа
Для простоты изложения будем считать, что граф задан матрицей смежности, которая хранится в квадратном массиве sm. Дополнительный линейный массив mark хранит информацию о последовательности посещения вершин:
procedure preorder_graph(v: byte);
var i: byte;
begin
k:= k+1;
mark[v]:= k; {текущей вершине v присвоен порядковый номер}
for i:= 1 to n do
if (mark[i]=0)and(sm[v,i]=1) {есть ребро из текущей вершины v
в еще не помеченную вершину i}
then preorder_graph(i);
end;
begin
...
k:= 0;
preorder_graph(start); {Вызов из тела программы}
...
end.
Обратный обход
Другие названия
Постфиксный обход: результатом обратного обхода ДСА арифметического выражения будет постфиксный вариант записи этого выражения.
Обход в глубину "снизу вверх": название имеет смысл лишь в случае стандартного расположения дерева корнем кверху.
Алгоритм PostOrder
- Начать с корня дерева.
- Совершить обратный обход левого поддерева.
- Совершить обратный обход правого поддерева.
- Пометить текущую вершину.
Замечание: Этот алгоритм также может быть распространен на случай произвольного корневого дерева.
Рис. 3. Последовательность нумерации вершин при обратном обходе дерева
Реализация
procedure postorder(p:ukaz; k:integer);
begin if p^.left<>nil then postorder(p^.left,k+1);
if p^.right<>nil then postorder(p^.right,k+1)
p^.mark:=k;
end;
begin
...
postorder(root,1); {Вызов из тела программы}
...
end.
Обратный обход произвольного связного графа
Для простоты изложения будем считать, что граф задан матрицей смежности, которая хранится в квадратном массиве sm. Дополнительный линейный массив mark хранит информацию о последовательности обхода вершин, а массив posesh - о фактах их посещения:
procedure postorder_graph(v:byte);
var i: integer;
begin
posesh[v]:=1; {текущая вершина v стала посещенной}
for i:=1 to n do
if (posesh[i]=0)and(sm[v,i]=1) {есть ребро из текущей вершины v
в еще не помеченную вершину i}
then postorder_graph(i);
inc(k);
mark[v]:=k; {текущей вершине v
присвоен порядковый номер}
end;
begin
...
k:=0;
postorder_graph(start); {вызов из тела программы}
...
end.
Синтаксический обход
Другие названия
Инфиксный обход: результатом синтаксического обхода ДСА арифметического выражения будет инфиксный вариант записи этого выражения.
Обход "слева направо": название имеет смысл лишь в случае стандартного расположения дерева корнем кверху.
Алгоритм SyntOrder
- Начать с корня дерева.
- Совершить прямой обход левого поддерева.
- Пометить текущую вершину.
- Совершить прямой обход правого поддерева.
Замечание: Этот обход специфичен только для бинарных деревьев, поэтому невозможно применить его к произвольному графу, каркасом которого совершенно не обязательно будет именно бинарное дерево.
Реализация
procedure syntorder(p:ukaz; k:integer);
begin if p^.left<>nil then syntorder(p^.left,k+1);
p^.mark:=k;
if p^.right<>nil then syntorder(p^.right,k+1);
end;
begin
...
syntorder(root,1); {Вызов из тела программы}
...
end.
Обход в ширину
Последовательность обхода
- Пометить вершину 0-го уровня (корень дерева).
- Пометить все вершины 1-го уровня.
- Пометить все вершины 2-го уровня.
- ...
Рис. 4. Последовательность нумерации вершин при синтаксическом обходе дерева
Замечание: Этот алгоритм может быть естественным образом распространен и на случай произвольного корневого дерева.
Алгоритм WideOrder
- Занести в очередь5) корень дерева.
- Пока очередь не станет пустой, повторять следующие действия:
- удалить первый элемент из головы очереди;
- добавить в хвост очереди всех потомков удаленной вершины.
Реализация
Для простоты реализации вновь пополним структуру дерева полем next:ukaz, которое будет служить для связки очереди:
head:= root;
tail:= root;
k:= 0;
repeat
tail^.next:= head^.left;
if head^.left<>nil then tail:= tail^.next;
tail^.next:= head^.right;
if head^.right<>nil then tail:= tail^.next;
inc(k);
head^.znachenie:= k; {можно write(head^.znachenie);}
head:= head^.next
until head = nil;
Рис. 5. Последовательность нумерации вершин при обходе дерева в ширину
Древесная сортировка
Задача. Упорядочить заданный набор (возможно, с повторениями) некоторых элементов (чисел, слов, т.п.).
Алгоритм TreeSort
- Для сортируемого множества элементов построить дерево двоичного поиска:
- первый элемент занести в корень дерева;
- для всех остальных элементов: начать проверку с корня; двигаться влево или вправо (в зависимости от результата сравнения с текущей вершиной дерева) до тех пор, пока не встретится такой же элемент, либо пока не встретится nil. Во втором случае нужно создать новый лист в дереве, куда и будет записано значение нового элемента.
- Совершить синтаксический обход построенного дерева, печатая каждую встреченную вершину столько раз, сколько было ее вхождений в сортируемый набор.
Реализация
Мы приведем реализацию первого шага алгоритма, сортирующего числа (для элементов другой природы потребуется изменить только процесс считывания):
new(root);
read(f,root^.chislo);
root^.kol:= 1;
root^.left:= nil;
root^.right:= nil;
while not eof(f) do
begin
read(f,x);
p:= root;
while true do
begin
if x = p^.chislo
then begin inc(p^.kol);
break
end;
if x > p^.chislo
then if p^.right <> nil
then p:= p^.right
else begin new(p^.right);
p:= p^.right;
p^.chislo:= x;
p^.kol:= 1;
p^.left:= nil;
p^.right:= nil;
break
end
(* x < p^.chislo *)
else if p^.left <> nil
then p:= p^.left
else begin new(p^.left);
p:= p^.left;
p^.chislo:= x;
p^.kol:= 1;
p^.left:= nil;
p^.right:= nil;
break
end
end;
end;
Подсчет количества компонент связности
Задача. Определить количество компонент связности в заданном графе.
Рекурсивный алгоритм
Считаем, что граф задан матрицей смежности sm.
Каждый элемент специального линейного массива mark будет хранить номер компоненты связности, к которой принадлежит соответствующая вершина графа.
Алгоритм КомпСвяз-Рек
- Совершить обход в глубину всех компонент связности графа, помечая вершины каждой из них отдельным номером.
Рекурсивная процедура обхода в глубину (прямого или обратного обхода) переберет все вершины, достижимые из начальной. Начальной вершиной для очередной компоненты связности может стать любая вершина, еще не отнесенная ни к какой другой компоненте связности (то есть еще не помеченная в массиве mark).
По окончании работы программы переменная kol будет содержать количество найденных компонент связности.
Реализация
procedure step (v: integer);
var j: integer;
begin
mark[v]:= k;
for j:=1 to N do
if (mark[j]=0)and(sm[v,j]<>0) then step(j);
end;
begin
...
for i:= 1 to N do mark[i]:=0;
k:= 0; {номер текущей компоненты связности}
for i:= 1 to N do
if mark[i]=0 then
begin inc(k);
step(i);
end;
...
end.
Итеративный алгоритм
Для этого алгоритма удобно, чтобы граф был представлен списком ребер.
Массив mark, как и прежде, будет хранить номера компонент связностей, к которым принадлежат помеченные вершины графа.
Алгоритм КомпСвяз-Итер
Прочитать начало и конец очередного ребра. Далее возможны 4 различные ситуации:
- Оба конца ребра еще не относятся ни к одной из ранее встретившихся компонент связности (mark[u]=0 и mark[v]=0). В этом случае количество компонент связности kol увеличивается на единицу, а новая компонента связности получает очередной номер ks+1.
- Один конец ребра уже относится к какой-то компоненте связности, а второй - еще нет (mark[u]=0, а mark[v]<>0). В этом случае общее количество компонент связности kol остается прежним, а непомеченный конец ребра получает ту же пометку, что и второй его конец.
- Оба конца нового ребра относятся к одной и той же компоненте связности (mark[u]= mark[v]<>0). В этом случае не нужно производить никаких действий.
- Концы нового ребра относятся к разным компонентам связности (0
mark[u]
mark[v]
0). В этом случае нужно объединить две ранее созданные компоненты связности в одну. Общее количество компонент связности kol уменьшается на 1, а все вершины, принадлежавшие к более новой компоненте связности (больший номер), получают новую пометку. Заметим, что переменная ks, обозначающая очередной свободный номер для следующей компоненты связности, в данном случае изменяться не должна, поскольку нет никакой гарантии, что изменен будет номер именно самой последней компоненты связности.
По окончании работы этого алгоритма в массиве mark будет записано S различных целых чисел, каждое из которых будет означать отдельную компоненту связности. Кроме того, в массиве могут остаться нулевые компоненты: каждая из них будет соответствовать изолированной вершине, которая тоже является отдельной компонентой связности. Следовательно, количество нулей должно быть прибавлено к количеству компонент, найденному в процессе работы основного алгоритма.
Реализация
kol:=0;
ks:=0;
while not eof(f) do
begin
readln(f,u,v);
if mark[u]=0
then if mark[v]=0
then begin {случай 1}
inc(kol);
inc(ks);
mark[u]:= ks;
mark[v]:= ks;
end
else mark[u]:= mark[v] {случай 2}
else if mark[v]=0
then mark[v]:= mark[u]
{случай 2 - симметричный}
else if mark[u]<>mark[v] {случай 4}
then begin
max:= v;
min:= u;
if u>v then begin
max:= u;
min:= v end;
for i:= 1 to n do
if mark[i]= max
then mark[i]:= min;
dec(kol);
end
end;
for i:=1 to N do
if mark[i]=0 then inc(kol);
Сравнение алгоритмов КомпСвяз-Рек и КомпСвяз-Итер
В худшем случае (при полном графе) рекурсивный алгоритм, перебирая все возможные ребра, будет вынужден вызвать основную процедуру (N-1)! раз. Велика вероятность, что при достаточно большом N произойдет переполнение оперативной памяти, которое вызовет аварийную остановку программы. Кроме того, размеры квадратной матрицы смежности дают сильное ограничение на возможное количество вершин графа: не более 250 (см. лекцию 3).
Итеративный же алгоритм переберет все ребра графа, которых может быть не более чем N*(N+1)/2. В половине этих случаев возможна ситуация объединения двух компонент связности в одну, для чего потребуется еще N операций. Следовательно, общая сложность алгоритма может быть приблизительно оценена значением N3/8. Возможное количество вершин графа ограничено только максимальным размером линейного массива (32 000).
Нахождение минимального каркаса
Задача. В заданном взвешенном связном графе определить множество ребер, составляющих некоторый его оптимальный каркас (например, минимальный по сумме весов входящих в него ребер).
Рекурсивный алгоритм
Алгоритм Каркас-Рек
Этот алгоритм базируется на прямом обходе графа, который учитывает два условия: во-первых, чтобы суммарный вес текущего каркаса был меньше текущего минимума и, во-вторых, чтобы в каркасе было ровно N-1 ребро (N - количество вершин графа).
Реализация
procedure step(v,k: byte; r: longint);
var j: byte;
begin
if r < min then
if k = N-1
then min:= r
else for j:= 1 to N do
if (sm[v,j]<>0)and(mark[j]=0)
then begin
mark[j]:= 1;
step(j,k+1,r+sm[v,j]);
mark[j]:= 0
end;
end;
begin
...
for i:= 1 to N do mark[i]:= 0;
min:= MaxLongInt;
for i:= 1 to N do
begin mark[i]:=1;
step(i,1,0);
mark[i]:=0;
end;
writeln(min);
...
end.
Для того чтобы помимо суммарного веса каркаса алгоритм также запоминал включенные в каркас ребра, необходимо добавить дополнительный квадратный массив, в котором будут храниться пометки включения ребер в каркас.
Итеративный алгоритм
Алгоритм Краскала
- Упорядочить все ребра графа по возрастанию их весов.
- Применить алгоритм КомпСвяз-Итер (см. пункт "Подсчет количества компонент связности").
Замечание: Выполнение алгоритма Краскала можно завершить сразу же, как только в каркас будет добавлено (N-1)-е ребро (поскольку в дереве с N вершинами должно быть ровно N-1 ребро).
Реализация
Реализация основной части алгоритма (шаг 2) совпадает с реализацией алгоритма КомпСвяз-Итер, за исключением того, что в случаях 1, 2 и 4 необходимо ввести подсчет добавленных в каркас ребер, а внешний цикл завершить не в момент достижения конца файла, а в момент, когда счетчик добавленных ребер станет равным N-1.
Нахождение кратчайших путей
Задача. В заданном взвешенном связном графе найти расстояние (длину кратчайшего пути) от выделенной вершины s до вершины t. Веса всех ребер строго положительны.
Рекурсивный алгоритм
Алгоритм Расст-Рек
Совершить обход графа в глубину, при каждом "шаге вперед" прибавляя длину ребра к длине текущего пути, при каждом возврате - отнимая длину этого ребра от длины текущего пути. При движении "вперед" пометки посещенности вершин ставятся, при "откате" - снимаются. По достижении выделенной вершины t производится сравнение длины текущего пути с ранее найденным минимумом.
Реализация
Пусть граф задан матрицей смежности sm, а массив mark хранит информацию о посещениях вершин. Напомним, что уменьшение длины пути "на возврате" совершается рекурсией автоматически, поскольку в ее заголовке использован параметр-значение, а вот аналогичное обнуление соответствующих позиций массива mark приходится делать вручную, поскольку задавать массив параметром-значением чересчур накладно:
procedure rasst(v: byte; r: longint);
var i: byte;
begin
if v = t
then if r< min then min:= r
else
else for i:= 1 to N do
if (mark[i]=0)and(sm[v,i]<>0)
then begin mark[i]:=1;
rasst(i,r+sm[v,i]);
mark[i]:=0
end
end;
begin
...
for i:= 1 to N do mark[i]:= 0;
min:= MaxLongInt;
mark[s]:= 1;
rasst(s,0);
mark[s]:= 0;
...
end.
Итеративный алгоритм
Алгоритм, предложенный Дейкстрой, настолько мощнее рекурсивного алгоритма Расст-Рек, что, при тех же начальных условиях и не прикладывая дополнительных усилий, он может найти расстояние от выделенной вершины s не только до одной вершины t, но и до всех остальных вершин графа.
Итак, пусть граф задан матрицей смежности.
Линейный массив dist будет хранить длины текущих путей от вершины s до всех остальных вершин. В начале этот массив будет инициирован числами MaxLongInt, символизирующими "бесконечность". По окончании работы алгоритма в этом массиве останутся только минимальные значения длин путей, которые и являются расстояниями.
Еще один линейный массив done потребуется нам для того, чтобы хранить информацию о том, найден ли уже минимальный путь (он же расстояние) до соответствующей вершины и можно ли исключить эту вершину из дальнейшего рассмотрения.
Переменная last будет хранить номер последней помеченной вершины.
Отметим особо, что на каждом шаге Алгоритм Дейкстры находит длину кратчайшего пути до очередной вершины графа. Именно поэтому достаточно сделать ровно N-1 итераций.
Алгоритм Дейкстры
-
Расстояние от s до s, конечно же, равно 0. Кроме того, это расстояние уже никогда не сможет стать меньше - ведь веса всех ребер графа у нас положительны. Таким образом:
dist[s]:= 0; done[s]:= true; last:= s;
-
Повторить N-1 раз следующие действия:
-
для всех непомеченных вершин х, связанных ребром с вершиной last, необходимо пересчитать расстояние:
dist[x]:= min(dist[x], dist[last]+ sm[last,x]);
- среди всех непомеченных вершин найти минимум в массиве dist: это будет новая вершина last;
- пометить эту новую вершину в массиве done.
-
Реализация
Мы надеемся, что функцию поиска меньшего из двух целых чисел min, использованную в тексте программы, читатели смогут написать самостоятельно.
dist[s]:= 0;
done[s]:= true;
last:= s;
for i:= 1 to N-1 do
begin
for x:= 1 to N do
if (sm[last,x]<>0)and(not done[x])
then dist[x]:= min(dist[x],dist[last]+ sm[last,x]);
min_dist:= MaxLongInt;
for x:= 1 to N do
if (not done[x])and(min_dist>dist[x])
then begin min_dist:= dist[x];
last:= x;
end;
done[last]:= true;
end.
Сравнение алгоритмов Расст-Рек и Дейкстры
Сложность рекурсивного алгоритма пропорциональна N!, а алгоритм Дейкстры имеет сложность ~N2. Комментарии, как говорится, излишни.
Элементы теории графов: основные понятия и определения. Способы представления графов и деревьев. Примеры применения деревьев в программировании. Примеры алгоритмов обработки деревьев и графов. Сравнение рекурсивных и итеративных алгоритмов, решающих некоторые классические задачи теории графов.
Чуть-чуть истории
Теория графов - довольно молодая наука (по сравнению, скажем, с геометрией). В 1736 году Санкт-Петербургская академия наук опубликовала труд Леонарда Эйлера, где рассматривалась задача о кенигсбергских1) мостах ("Можно ли, пройдя все городские мосты ровно по одному разу, вернуться в исходную точку?"). Это была первая работа по будущей теории графов.
Особенно приятно то, что в данном случае Россия - родина пусть и не новой породы слонов, но зато нового научного направления!
Графы: определения и примеры
Итак, перейдем к изложению некоторых понятий современной теории графов.
Неориентированные графы
Граф - это двойка <V, E>, где V - непустое множество вершин, а Е - множество ребер, соединяющих эти вершины попарно2). Две вершины, связанные между собой ребром, равноправны, и именно поэтому такие графы называются неориентированными: нет никакой разницы между "началом" и "концом" ребра.
Говоря простым языком, граф - это множество точек (для удобства изображения - на плоскости) и попарно соединяющих их линий (не обязательно прямых). В графе важен только факт наличия связи между двумя вершинами. От способа изображения этой связи структура графа не зависит.
Например, три графа на рис. 1 совпадают, а два графа на рис. 2 - различны.
Рис. 1. Три способа изображения одного графа
Из приведенного выше определения вытекает, что в графах не бывает петель - ребер, соединяющих некоторую вершину саму с собой. Кроме того, в классическом графе не бывает двух различных ребер, соединяющих одну и ту же пару вершин.
Ребро е и вершина v называются инцидентными друг другу, если вершина v является одним из концов ребра е.
Рис. 2. Пример двух разных графов
Рис. 3.
Псевдограф
Любому ребру инцидентно ровно две вершины, а вот вершине может быть инцидентно произвольное количество ребер, это количество и определяет степень вершины. Изолированная вершина вообще не имеет инцидентных ей ребер (ее степень равна 0).
Две вершины называются смежными, если они являются разными концами одного ребра (иными словами, эти вершины инцидентны одному ребру). Аналогично, два ребра называются смежными, если они инцидентны одной вершине.
Путь в графе - это последовательность вершин (без повторений), в которой любые две соседние вершины смежны. Например, в графе, изображенном на рис. 1, есть два различных пути из вершины a в вершину с: adbc и abc.
Вершина v достижима из вершины u, если существует путь, начинающийся в u и заканчивающийся в v.
Граф называется связным, если все его вершины взаимно достижимы.
Компонента связности - это максимальный связный подграф. В общем случае граф может состоять из произвольного количества компонент связности. Заметим, что любая изолированная вершина является отдельной компонентой связности. На рис. 4 изображен граф, состоящий из четырех компонент связности: [abhk], [gd], [c] и [f].
Длина пути - количество ребер, из которых этот путь состоит. Например, длина уже упомянутых путей adbc и abc - 3 и 2 соответственно.
Рис. 4. Несвязный граф
Говорят, что вершина v принадлежит k-му уровню относительно вершины u, если существует путь из u в v длиной ровно k ребер. Одна и та же вершина может относиться к разным уровням. Например, в графе, изображенном на рис. 1, относительно вершины a существует 4 уровня:
- 0) a;
- 1) b, d;
- 2) b, d, c (пути adb, abd, abc);
- 3) c (путь adbc).
Расстояние между вершинами u и v - это длина кратчайшего пути от u до v. Из этого определения видно, что расстояние между вершинами a и c в графе на рис. 1 равно 2.
Цикл - это замкнутый путь. Все вершины в цикле, кроме первой и последней, должны быть различны. Например, циклом является путь abda в графе на рис. 1.
Эйлеров граф - это граф, в котором существует путь или цикл, содержащий все ребра графа (вершины могут повторяться). Именно такие графы положительно решают упомянутую в начале лекции задачу о кенигсбергских мостах. Например, граф на рис. 5 является Эйлеровым: искомым путем в нем будет dbacfbcd.
Рис. 5. Граф Эйлера
Гамильтонов граф - это граф, в котором существует путь или цикл (без повторений), содержащий все вершины графа (см. рис. 5; искомый цикл: abdfca).
Ориентированные графы
Орграф - это граф, все ребра которого имеют направление. Такие направленные ребра называются дугами. На рисунках дуги изображаются стрелочками (см. рис. 6).
Рис. 6. Орграф
В отличие от ребер, дуги соединяют две неравноправные вершины: одна из них называется началом дуги (дуга из нее исходит), вторая - концом дуги (дуга в нее входит). Можно сказать, что любое ребро - это пара дуг, направленных навстречу друг другу.
Если в графе присутствуют и ребра, и дуги, то его называют смешанным.
Все основные понятия, определенные для неориентированных графов (инцидентность, смежность, достижимость, длина пути и т.п.), остаются в силе и для орграфов - нужно лишь заменить слово "ребро" словом "дуга". А немногие исключения связаны с различиями между ребрами и дугами.
Степень вершины в орграфе - это не одно число, а пара чисел: первое характеризует количество исходящих из вершины дуг, а второе - количество входящих дуг.
Путь в орграфе - это последовательность вершин (без повторений), в которой любые две соседние вершины смежны, причем каждая вершина является одновременно концом одной дуги и началом следующей дуги. Например, в орграфе на рис. 6 нет пути, ведущего из вершины 2 в вершину 5. "Двигаться" по орграфу можно только в направлениях, заданных стрелками.
| Орграф | Вершины | Дуги |
|---|---|---|
| Чайнворд | Слова | Совпадение последней и первой букв (возможность связать два слова в цепочку) |
| Стройка | Работы | Необходимое предшествование (например, стены нужно построить раньше, чем крышу, т. п.) |
| Обучение | Курсы | Необходимое предшествование (например, курс по языку Pascal полезно изучить прежде, чем курс по Delphi, и т.п.) |
| Одевание ребенка | Предметы гардероба | Необходимое предшествование (например, носки должны быть надеты раньше, чем ботинки, и т.п.) |
| Европейский город | Перекрестки | Узкие улицы с односторонним движением |
| Организация | Сотрудники | Иерархия (начальник - подчиненный) |
Взвешенные графы
Взвешенный (другое название: размеченный) граф (или орграф) - это граф (орграф), некоторым элементам которого (вершинам, ребрам или дугам) сопоставлены числа. Наиболее часто встречаются графы с помеченными ребрами. Числа-пометки носят различные названия: вес, длина, стоимость.
Замечание: Обычный (не взвешенный) граф можно интерпретировать как взвешенный, все ребра которого имеют одинаковый вес 1.
Длина пути во взвешенном (связном) графе - это сумма длин (весов) тех ребер, из которых состоит путь. Расстояние между вершинами - это, как и прежде, длина кратчайшего пути. Например, расстояние от вершины a до вершины d во взвешенном графе, изображенном на рис. 7, равно 6.
Рис. 7. Взвешенный граф
N-периферия вершины v - это множество вершин, расстояние до каждой из которых (от вершины v) не меньше, чем N.
| Граф | Вершины | Вес вершины | Ребра (дуги) | Вес ребра (дуги) |
|---|---|---|---|---|
| Таможни | Государства | Площадь территории | Наличие наземной границы | Стоимость получения визы |
| Переезды | Города | Стоимость ночевки в гостинице | Дороги | Длина дороги |
| Супер-чайнворд | Слова | - | Совпадение конца и начала слов(возможность "сцепить" слова) | Длина пересекающихся частей |
| Карта | Государства | Цвет на карте | Наличие общей границы | - |
| Сеть | Компьютеры | - | Сетевой кабель | Стоимость кабеля |
Способы представления графов
Существует довольно большое число разнообразных способов представления графов. Однако мы изложим здесь только самые полезные с точки зрения программирования.
Матрица смежности
Матрица смежности Sm - это квадратная матрица размером NxN (N - количество вершин в графе), заполненная единицами и нулями по следующему правилу:
Если в графе имеется ребро e, соединяющее вершины u и v, то Sm[u,v] = 1, в противном случае Sm[u,v] = 0.
Заметим, что данное определение подходит как ориентированным, так и неориентированным графам: матрица смежности для неориентированного графа будет симметричной относительно своей главной диагонали, а для орграфа - несимметричной.
Задать взвешенный граф при помощи матрицы смежности тоже возможно. Необходимо лишь внести небольшое изменение в определение:
Если в графе имеется ребро e, соединяющее вершины u и v, то Sm[u,v] = ves(e), в противном случае Sm[u,v] = 0.
Это хорошо согласуется с замечанием, сделанным в предыдущем пункте: невзвешенный граф можно интерпретировать как взвешенный, все ребра которого имеют одинаковый вес 1.
Небольшое затруднение возникнет в том случае, если в графе разрешаются ребра с весом 0. Тогда придется хранить два массива: один с нулями и единицами, которые служат показателем наличия ребер, а второй - с весами этих ребер.
В качестве примера приведем матрицы смежности для трех графов, изображенных на рис. 5, рис. 6 и рис. 7 (см. рис. 8).

















