Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2178 / 0 | Длительность: 63:16:00
Лекция 17:

Виды графов и их свойства

АТД графа

Для разработки алгоритмов обработки графов нам понадобится абстрактный тип данных (АТД, см. "Абстрактные типы данных" ), который позволит нам формулировать фундаментальные задачи. Программа 17.1 представляет собой интерфейс этого АТД, а базовые представления и реализации графа для этого АТД рассматриваются в разделах 17.3—17.5. Далее в этой книге всякий раз, когда мы будем встречаться с новыми задачами обработки графов, мы будем рассматривать алгоритмы их решения и реализации этих алгоритмов в контексте клиентских программ и абстрактных типов данных, которые обращаются к графам через этот интерфейс. Такая схема позволит решать разнообразные задачи обработки графов, от элементарного сопровождения до сложных решений трудных задач.

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

Программа 17.1. Интерфейс АТД графа

Этот интерфейс -отправная точка для реализации и тестирования алгоритмов на графах. Он определяет два типа данных: тривиальный тип Edge (ребро) с функцией конструктора, которая создает ребро из двух заданных вершин, и тип GRAPH, определенный в соответствии со стандартной методологией независимости интерфейса АТД от представления (см. "Абстрактные типы данных" ).

Конструктор GRAPH принимает два аргумента: целое число, задающее количество вершин, и логическое значение, указывающее, является ли граф ориентированным или неориентированным (орграф) -по умолчанию граф неориентированный.

Базовые операции, необходимые для обработки графов и орграфов -функции АТД для их создания и уничтожения, подсчета количества вершин и ребер, а также добавления и удаления ребер. Класс итератора adjlterator позволяет клиентам выполнить обработку всех вершин, смежных с заданной. Его использование демонстрируется в программах 17.2 и 17.3.

    struct Edge
      { int v, w;
        Edge(int v = -1, int w = -1) : v(v), w(w) { }
      };
    class GRAPH
      { private:
        // Код, зависящий от реализации
      public:
          GRAPH(int, bool);
        ~GRAPH();
        int V() const;
        int E() const;
        bool directed() const;
        int insert(Edge);
        int remove(Edge);
        bool edge(int, int);
        class adjIterator
          { public:
            adjIterator(const GRAPH &, int);
            int beg();
            int nxt();
            bool end();
          };
        };
      

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

Конструктор графа принимает в качестве аргумента максимально возможное количество вершин графа, чтобы реализации могли выделить необходимый объем памяти.

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

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

Программа 17.2 содержит функцию, которая демонстрирует применение класса итератора из АТД графа. Эта функция извлекает из заданного графа множество его ребер и возвращает их в переменной типа vector из библиотеки STL (Standard Template Library -стандартная библиотека шаблонов) C++. По сути граф есть просто множество ребер, и довольно часто требуется получить граф именно в таком виде, независимо от его внутреннего представления.

Программа 17.2. Пример клиентской функции обработки графов

Данная функция демонстрирует один из способов использования АТД графа для реализации базовой операции обработки графов, не зависимой от представления. Она возвращает все ребра графа в виде вектора.

Эта реализация служит иллюстрацией основного способа работы большинства программ, которые мы будем рассматривать: для перебора всех ребер графа перебираются все вершины, смежные с каждой вершиной этого графа. Функции beg, end и nxt обычно не вызываются никаким другим способом, кроме продемонстрированного в этой программе -это позволяет лучше оценить характеристики производительности наших реализаций (см. раздел 17.5).

  template <class Graph>
  vector <Edge> edges(Graph &G)
    { int E = 0;
      vector <Edge> a(G.E());
      for (int v = 0; v < G.V(); v++)
        { typename Graph::adjIterator A(G, v);
          for (int w = A.beg(); !A.end(); w = A.nxt())
            if (G.directed() || v < w)
              a[E++] = Edge(v, w);
        }
      return a;
    }
      

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

Программа 17.3 -другой пример использования класса итератора из АТД графа, она предназначена для вывода таблицы вершин, смежных с каждой вершиной (см. рис. 17.7 рис. 17.7). Коды этих двух примеров довольно похожи, как похожи и многие другие программные реализации многочисленных алгоритмов обработки графов. Даже удивительно, что все алгоритмы, изучаемые в данной книге, можно построить, как и приведенные функции, на этой базовой абстракции обработки всех вершин, смежных с каждой вершиной (что эквивалентно обработке всех ребер в графе).

Как сказано в разделе 17.5, мы часто оформляем логически взаимосвязанные функции в отдельный класс. Программа 17.4 представляет собой интерфейс такого класса. В ней содержится определение функции show из программы 17.3, а также двух других функций, которые вставляют в граф ребра, считанные из стандартного ввода (реализации этих функций см. в упражнении 17.12 и программе 17.14).

Задачи обработки графов, рассматриваемые в этой книге, обычно принадлежат к одной из трех обширных категорий:

  • Вычисления значения какой-то меры графа.
  • Выбор некоторого подмножества ребер графа.
  • Ответы на вопросы о каких-то свойствах графа.

Примеры задач из первой категории -количество связных компонентов и длина кратчайшего пути между двумя заданными вершинами графа; примеры задач из второй категории -остовное дерево и цикл наибольшей длины, который содержит заданную вершину; примеры задач из третьей категории -определение, принадлежат ли заданные вершины одному и тому же компоненту. Достаточно вспомнить термины, определенные в разделе 17.1, которые наводят на множество вычислительных задач.

 Формат списка смежности

Рис. 17.7. Формат списка смежности

Эта таблица служит еще одним способом представления графа, приведенного на рис. 17.1; с каждой вершиной связывается множество смежных с ней вершин (которые соединены с ней одним ребром). Каждое ребро принадлежит двум множествам: для каждого ребра u-v графа вершина u содержится в множестве, связанном с вершиной v, а вершина v содержится в множестве, связанном с вершиной u.

Программа 17.3. Клиентская функция вывода графа

Данная реализация функции show для класса IO из программы 17.4 использует АТД графа для вывода таблицы вершин, смежных с каждой вершиной графа. Порядок перечисления вершин в таблице зависит от представления графа и реализации АТД (см. рис. 17.7).

  template <class Graph>
  void IO<Graph>::show(const Graph &G)
    { for (int s = 0; s < G.V(); s++)
        { cout.width(2); cout << s << ":";
          typename Graph::adjIterator A(G, s);
          for (int t = A.beg(); !A.end(); t = A.nxt())
            { cout.width(2); cout << t << " "; }
          cout << endl;
        }
    }
      

Программа 17.4. Интерфейс ввода/вывода для обработки графов

Этот класс демонстрирует оформление схожих функций в единый класс. Он определяет функции, выполняющие вывод графа (см. программу 17.3), вставку ребер, определяемых парами целых чисел из стандартного ввода (см. упражнение 17.12) и вставку ребер, определяемых парами символов из стандартного ввода (см. программу 17.14).

  template <class Graph>
  class IO
    { public:
      static void show(const Graph &);
      static void scanEZ(Graph &);
      static void scan(Graph &);
    };
      

Для решения таких задач мы будем строить абстрактные типы данных, которые являются клиентами базового АТД из программы 17.1, и которые, в свою очередь, позволяет отделить клиентские программы, требуемые для решения реальных задач, от их реализаций. Например, программа 17.5 представляет собой интерфейс для АТД связности графа. Мы можем написать клиентские программы, использующие этот АТД для создания объектов, которые вычисляют количество связных компонентов в графе и которые могут проверить, находятся ли любые две вершины в одном и том же связном компоненте. Описание реализаций такого АТД и характеристики их производительности будут даны в "Поиск на графе" . Подобные АТД мы будем разрабатывать на протяжении всей книги. Как правило, такие АТД содержат общедоступную функцию-член, выполняющую предобработку (обычно это конструктор), приватные члены данных, которые содержат информацию, полученную во время предобработки, и общедоступные функции-члены обслуживания запросов, которые используют эту информацию для предоставления клиентам информации о графе.

В этой книге мы будем работать главным образом со статическими (static) графами, которые содержат фиксированное количество вершин V и ребер E.

Программа 17.5. Интерфейс связности

Данный интерфейс АТД демонстрирует типичную парадигму, которую мы используем для реализации алгоритмов обработки графов. Он позволяет клиентам создавать объекты, которые выполняют обработку графа для определения ответов на запросы, касающиеся связности этого графа. Функция-член count возвращает количество связных компонентов графа, а функция-член connect проверяет, связаны ли две заданные вершины. Реализация этого интерфейса приведена в программе 18.4.

  template <class Graph>
  class CC
    { private:
      // Код, зависящий от реализации 
      public:
        CC(const Graph &);
        int count();
        bool connect(int, int);
    };
      

Обычно мы строим графы с помощью E вызовов функции insert, а затем обрабатываем их -либо вызвав соответствующую функцию АТД, которая принимает граф в качестве аргумента и возвращает некоторую информацию о графе, либо используя объекты наподобие вышеуказанных для предварительной обработки графа, которые позволяют затем эффективно отвечать на запросы, касающиеся графа. В любом случае, после изменения графа функциями insert и remove необходима повторная обработка графа. Динамические задачи, где обработка графов может чередоваться с добавлением или удалением вершин и ребер графа, принадлежат к онлайновым алгоритмам (on-line algorithm), известных также как динамические алгоритмы (dynamic algorithm), с которыми связаны другие сложные задачи. Например, задача связности, которую мы решали с помощью алгоритма объединения-поиска в "Введение" , представляет собой пример интерактивного алгоритма, поскольку мы можем получать информацию о связности графа в процессе включения ребер в этот граф. АТД из программы 17.1 поддерживает операции вставить ребро и удалить ребро, и клиенты могут использовать их для внесения изменений в графы. Однако некоторые последовательности операций могут снизить производительность. Например, алгоритмы объединения-поиска могут потребовать повторной обработки всего графа, если клиент удалил из него ребро. В большинстве задач обработки графов, которые мы будем рассматривать, добавление или удаление нескольких ребер может кардинально изменить вид графа, а, значит, понадобится его повторная обработка.

Одна из наиболее важных задач обработки графов заключается в получении четких характеристик производительности и гарантировании, что они правильно используются клиентскими программами. Как и в случае более простых задач, которые рассматривались в частях I—IV, наша методика использования АТД позволяет системно решать эти задачи.

Пример клиентской программы обработки графов приведен в программе 17.6. Она использует базовый АТД из программы 17.1, класс ввода/вывода из программы 17.4 для считывания графа из стандартного ввода и его вывода на стандартное устройство вывода, а также класс связности из программы 17.5 для определения количества его связных компонентов. Аналогичные, хотя и более сложные, клиентские программы мы будем использовать для построения других видов графов, тестирования алгоритмов, изучения других свойств графов и использования графов для решения других задач. Эту базовую схему можно применять в любых приложениях обработки графов.

В разделах 17.3—17.5 мы рассмотрим основные классические представления графа и реализации функций АТД из программы 17.1. Эти реализации позволят расширить интерфейс, чтобы охватить задачи обработки графов, которыми мы будем заниматься на протяжении ряда последующих глав.

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

Программа 17.6. Пример клиентской программы обработки графов

Эта программа демонстрирует использование абстрактных типов данных, описанных в настоящем разделе и использующих соглашения об АТД, которые были сформулированы в "Абстрактные типы данных" . Она создает граф с V вершинами, вставляет в него ребра, получаемые из стандартного ввода, выводит его, если граф не слишком велик, и вычисляет (и выводит) количество связных компонентов. Предполагается, что программы 17.1, 17.4 и 17.5 (с реализациями) содержатся соответственно в файлах GRAPH.cc, IO.cc и CC.cc.

  #include <iostream.h>
  #include <stdlib.h>
  #include "GRAPH.cc"
  #include "IO.cc"
  #include "CC.cc"
  main(int argc, char *argv[])
    { int V = atoi(argv[1]);
      GRAPH G(V);
      IO<GRAPH>::scan(G);
      if (V < 20) IO<GRAPH>::show(G);
      cout << G.E() << " ребер ";
      CC<GRAPH> Gcc(G);
      cout << Gcc.count() << " компонентов" << endl;
    }
      

Например, в качестве базы для реализации АТД можно рассмотреть представление в виде вектора ребер (см. упражнение 17.16). Это прямое представление несложно реализовать, однако оно не позволяет эффективно выполнять базовые операции обработки графов, к изучению которых мы вскоре приступим. Как мы увидим, большинство приложений обработки графов могут неплохо работать с одним из двух элементарных классических представлений, которые ненамного сложнее представления вектором ребер -матрица смежности и списки смежности. Эти представления графов, которые мы подробно изучим в разделах 17.3 и 17.4, основаны на элементарных структурах данных (и мы их рассматривали в "Элементарные структуры данных" и 5 в качестве примеров применения последовательного и связного распределения). Выбор одного из этих представлений зависит главным образом от того, является ли граф насыщенным или разреженным, хотя, как обычно, важную роль при принятии решения играет также характер выполняемых операций.

Упражнения

17.12. Разработайте реализацию функции scanEZ из программы 17.4: напишите функцию, которая строит граф, считывая ребра (пары целых чисел в диапазоне от 0 до V-1) из стандартного ввода.

17.13. Напишите клиентскую программу для АТД графа, которая добавляет ребра из заданного вектора в заданный граф.

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

17.15. Разработайте реализацию для АТД связности из программы 17.5, используя алгоритм объединения-поиска (см. "Введение" )

17.8. Приведите реализацию функций из программы 17.1, которая использует для представления графа вектор ребер. Используйте примитивную реализацию функции, которая для удаления ребра v-w просматривает вектор, находит в нем ребро v-w или w-v и затем меняет местами найденное ребро с последним ребром вектора. Используйте аналогичный просмотр для реализации итератора. Примечание: предварительное чтение раздела 17.3 упростит вашу задачу.

Бактыгуль Асаинова
Бактыгуль Асаинова

Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат?

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?