Смекни!
smekni.com

AGraph: библиотека классов для работы с помеченными графами (стр. 3 из 6)

Операция Эффективность
Списки Массивы
Добавление вершины | ребра O(1) O(n) | O(m) в худшем случае,
O(1) в среднем
Удаление вершины |
ребра
O(1) O(n) | O(m)
Доступ к вершине | ребру по индексу O(n) | O(m) O(1)

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

Достоинством динамических массивов является быстрый доступ к элементам по индексу. Наиболее "дорогой" операцией при использовании динамических массивов является добавление элемента, поскольку в худшем случае для этого требуется выделить новый блок памяти увеличенного размера, скопировать содержимое старого блока памяти в новый и освободить старый блок, причем, по крайней мере, этап копирования имеет сложность O(n). В то же время, в среднем операция добавления вершин (ребер) в AGraph выполняется более эффективно. Это достигается за счет того, что при увеличении размера динамического массива (вектора) в библиотеке Vectors память выделяется сразу для нескольких элементов, поэтому в большинстве случаев операция добавления элемента не требует фактического изменения размера вектора.

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

4. Базовые средства

К базовым средствам библиотеки для работы с графами относятся средства, обеспечивающие выполнение наиболее общих операций над графом и его элементами, в том числе создание и уничтожение объектов-графов, добавление в граф вершин и ребер, удаление их из графа, итерацию по вершинам и ребрам и т.д. Базовые средства библиотеки AGraph в основном соответствуют аналогичным средствам других библиотек (см. пример 1). При создании программного интерфейса приложений (API) библиотеки AGraph первоочередное внимание уделялось обеспечению максимальных функциональных возможностей библиотеки при сохранении простоты ее использования. Имена классов библиотеки, их полей, методов и свойств (property) соответствуют распространенной англоязычной терминологии теории графов и общепринятым соглашениям языка Object Pascal.

// создание графаG:=TGraph.Create;// добавление вершин и реберV:=G.AddVertex;G.AddVertices(5);E:=G.AddEdge(G[0], G[1]); // ребро (v0, v1)G.AddEdgeI(0, 3); // ребро (v0, v3)G.AddEdges([1, 2, 2, 4]); // ребра (v1, v2) и (v2, v4)// итерация по вершинам графаfor I:=0 to G.VertexCount - 1 do begin V:=G.Vertices[I];// итерация по ребрам, инцидентным заданной вершине графа for J:=0 to V.Degree - 1 do With V.IncidentEdge[J] do {...} ;end;// итерация по ребрам графаfor I:=0 to G.EdgeCount - 1 do With G.Edges[I] do {...} ;// удаление ребра (v0, v1)E.Free;// удаление ребра (v1, v2)G.GetEdgeI(1, 2).Free;// удаление вершины (с инцидентными ребрами)G.Vertices[2].Free;// уничтожение графаG.Free;

Пример 1. Базовые операции над графами в библиотеке AGraph.

Если сравнивать библиотеки AGraph и LEDA, то можно отметить два существенных отличия. Первое из них связано с использованием динамических массивов для внутреннего представления графов в библиотеке AGraph. Массивы позволяют применять простой for-цикл для итерации по вершинам и ребрам графа. В библиотеке LEDA, использующей списки для хранения вершин и ребер, для той же цели необходимо использовать специальные макросы, а в библиотеке GTL (Passau), основанной на STL, - итераторы STL [библиотека LEDA также поддерживает "STL-style" итераторы (пока в качестве экспериментальной возможности)]. Второе отличие заключается в том, что в AGraph для удаления вершин и ребер из графа используется стандартный способ уничтожения объектов Object Pascal - вызов метода Free, в то время как в библиотеке LEDA для удаления вершин и ребер из графа приходится использовать специальные методы классов-графов.

Наиболее важным различием между библиотеками AGraph и GTL (Н-Новгород) является то, что в библиотеке GTL вершины и ребра существуют отдельно от графов и могут принадлежать сразу нескольким графам либо ни одному из графов. Для удаления неиспользуемых вершин (ребер) из памяти в GTL используется техника счетчиков ссылок (reference count): когда объект (вершина или ребро) присоединяется к графу или другой структуре (метод AddRef), счетчик увеличивается, когда удаляется из структуры (метод Release) - уменьшается. При удалении ссылки на объект из последней структуры, он должен удалить себя из памяти. Такое решение позволяет сэкономить память в тех случаях, когда графы конструируются из уже существующих объектов. Одновременно снимается проблема отождествления объектов "родственных" графов: например, в GTL порожденный подграф графа содержит те же вершины, что и сам граф (а не копии этих вершин!). В то же время, такая интерпретация вершин и ребер может затруднить использование библиотеки. Во-первых, проблемы могут возникнуть, когда с вершинами и ребрами графа связаны какие-либо атрибуты (см. ниже) - изменение атрибута вершины (ребра) одного графа может вызвать неожиданное для пользователя изменение атрибута вершины (ребра) другого графа. Во-вторых, возможность существования "автономных" вершин и ребер, на мой взгляд, противоречит интуитивному пониманию графа.

5. Использование атрибутов

Во многих случаях необходимо связать с вершинами и ребрами графа дополнительные данные - атрибуты (метки) вершин и ребер графа. Так, во взвешенном графе каждое ребро имеет целый или вещественный атрибут - вес ребра; в молекулярном графе с вершинами и ребрами может быть связан целый ряд атрибутов (для вершин - номер атома, представляемого данной вершиной графа, в периодической таблице элементов Д.И.Менделеева, заряд атома и др., для ребер - тип валентной связи, которой соответствует данное ребро).

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

// создание графаG:=TGraph.Create;V:=G.AddVertex;E:=G.AddEdge(V, G.AddVertex);// создание динамического массива для хранения целого атрибута вершин графа (первый параметр - количество элементов, второй - значение по умолчанию)AttrVector1:=TIntegerVector.Create(G.VertexCount, 0);// создание динамического массива для хранения строкового атрибута ребер графаAttrVector2:=TStrLst.Create;AttrVector2.Count:=G.EdgeCount;// присваивание значений атрибутам вершины V и ребра E графаAttrVector1[V.Index]:=1;AttrVector2[E.Index]:='AGraph';

Пример 2. Использование динамического массива для хранения атрибутов вершин и ребер графа в библиотеке AGraph.

В библиотеке LEDA для реализации аналогичного способа привязки атрибутов к вершинам и ребрам графа необходимо использовать специальные структуры данных - классы node_array и edge_array (либо отличные от них по реализации, но эквивалентные в данном контексте классы node_map и edge_map). Это связано с тем, что в LEDA объекты-вершины и объекты-ребра хранятся в списках, а не в динамических массивах.

// создание графаgraph G;node v = G.new_node();edge e = G.new_edge(v, G.new_node());// создание структуры node_arrray для хранения атрибута типа int для вершин графа Gnode_array attr1(G);// создание структуры edge_arrray для хранения атрибута типа string для ребер графа Gedge_array attr2(G);// присваивание значений атрибутам вершины v и ребра e графа Gattr1[v]:=5;attr2[e]:="Saarbruecken";

Пример 3. Использование классов node_array и edge_array для хранения атрибутов вершин и ребер графа в библиотеке LEDA.

Описанный способ хранения атрибутов подходит только для статических графов, т.к. при модификации графа соответствие между вершинами (ребрами) графа и элементами вспомогательной структуры данных теряется. Кроме того, определенные таким образом атрибуты не могут автоматически сохраняться при записи графа в поток или копироваться при копировании графов. Наиболее естественным способом "надежной" привязки атрибутов к вершинам (ребрам) графа является хранение атрибутов (или ссылок на атрибуты) непосредственно в объектах-вершинах (объектах-ребрах) графа. Библиотеки LEDA и GTL (University of Passay) предлагают для этого параметризуемый класс графов, библиотека GTL (Н-Новгород) - использование классов-"привкусов" (Flavor) и множественного наследования. Оба этих метода хорошо работают, когда набор атрибутов вершин (ребер) графа известен во время компиляции программы. В библиотеке AGraph реализованы еще более гибкие средства, позволяющие создавать и уничтожать атрибуты динамически, в процессе исполнения.

Параметризуемый класс графов в LEDA позволяет создавать графы, с каждой вершиной и каждым ребром которого связан атрибут заданного типа (см. пример 4). Доступ к таким атрибутам является в LEDA более эффективным, чем доступ к атрибутам, определенным с помощью node_array и edge_array.

// создание графаGRAPH H;node v = H.new_node();edge e = H.new_edge(v, H.new_node());// присваивание значений атрибутам вершины v и ребра e графа GH[v]:=5;H[e]:="Saarbruecken";

Пример 4. Использование параметризуемого класса графов LEDA.

В библиотеке GTL (Н-Новгород) для создания вершин и ребер с заданными свойствами используется механизм классов-"привкусов" (Flavor). Данный механизм может использоваться для привязки атрибутов к вершинам и ребрам графа, хотя его возможности этим не ограничиваются. Flavor - это абстрактный (чисто виртуальный в терминологии С++) класс, который применяется в качестве "добавки" при порождении новых классов с использованием множественного наследования. Flavor требует, чтобы объект обладал некоторыми свойствами, но не привносит их в объект сам. Например, класс CWeightedEdge ("взвешенное ребро") порождается от классов CEdge ("ребро") и специального класса CWeightFlavor. В классе CWeightFlavor определены два абстрактных виртуальных метода - SetWeight и GetWeight, которые должны быть перекрыты в классе CWeightedEdge. GTL предоставляет ряд "привкусов" для построения распространенных типов объектов (при необходимости пользователи могут расширить набор Flavor). Порожденные с помощью Flavor классы, в свою очередь, используются в качестве параметров шаблонных классов графов для создания классов графов, вершины и ребра которых обладают заданными свойствами (см. пример 5).