Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы объектно-ориентированного программирования задач на графах. Учебное пособие
.pdf
8.1. Иерархия и назначение функций классов
81
необходимо создать контруктор класса Task() для передачи значения количества вершин в абстрактный класс Graph.
Абстрактный класс Graph объявим следующим образом:
class Graph
{
protected: int N;
public: void output_N(){cout<<N;}
int return_N(){return N;}
virtual void input_graph() = 0;
virtual void output_graph() = 0;
virtual void rand_grаph()= 0;
Graph(int n1){N=n1;}
};
Объявление класса Matr, производного от класса Graph, будет иметь
вид:
сlass Matr : public Graph
{
protected: bool **matr;
public:
void input_graph();
void output_graph();
void rand_grаph();
Matr(int n1) : Graph(n1);
~Matr();
};
В классе Matr определим чистые виртуальные функции input_graph(), output_graph() и rand_grаph(). Для вызова кон-
структора класса Graph для передачи значения n1 – количества вершин графа в абстрактный класс Graph, объявим конструктор класса
Matr:
Matr(int n1):Graph(n1);

8. Программирование задач на графах повышенной сложности
82
Конструктор класса Matr также предназначен для создания двумерного
массива, моделирующего матрицу смежности, в динамической памяти.
Выполняться контруктор класса Matr будет после выполнения коструктора класса Graph и присвоения значения переменной N - количеству
вершин графа, объявленном в абстрактном классе Graph.
Конструктор и деструктор класса Matr, а также функции
input_graph(), output_graph() и rand_grаph() определим вне
класса Matr.
Оределение конструктора класса Matr будет следующим:
Matr::Matr(int n1):Graph(n1)
{
matr=new bool *[N];
for(int i=0; i<N;i++)
matr[i]= new bool [N];
}
Деструктор класса Matr предназначен для удаления двумерного
массива, моделирующего матарицу смежности, из динамической памяти:
Matr::~Matr()
{
for(int i=0; i<N;i++)
delete[] matr[i];
delete[] matr;
}
Функция input_graph() предназначена для ввода матрицы
смежности графа с клавиатуры:
void Matr::input_graph()
{
for(int i=0;i<N;i++)
for(int j=0;j< N;j++)
{ cout << " matr["<<i<<"]["<<j<<"]=";

8.1. Иерархия и назначение функций классов
83
cin >> matr[i][j];
}
cout <<endl;
}
Функция output_graph()предназначена для вывода на экран эле-
ментов матрицы смежности графа через пробел по строкам:
void Matr::output_graph()
{ for( int i=0;i< N;i++)
{
for( int j=0;j< N;j++)
cout << matr[i][j]<< ‘ ‘;
cout << endl;
}}
Более подробно остановимся на формировании случайной матрицы
смежности графа – создание, так называемого, случайного графа:
void Matr::rand_grаph()
{ for( int i=1;i< N-1;i++)
for( int j=i;j< N;j++)
matr[i][j]= matr[j][i]=rand()%2;
for( int i=0;i< N;i++)
matr[i][i]=0;
}
Первые два цикла предназначены для задания случайных значений 0
или 1 элементам двумерного массива, расположенным выше главной диагонали. Поскольку формируется симметрический граф, то эти же значения
присваиваются симметричным элементам массива:
matr[i][j]= matr[j][i]=rand()%2;
После этого элементам главной диагонали присваиваются нулевые
значения, поскольку симметрический граф не содержит петель. Напомним,
что индексы строки и столбца элемента главной диагонали имеют одинаковые значения:
matr[i][i]=0;

8. Программирование задач на графах повышенной сложности
84
8.2. Программирование функции определения паросочетания графа
Паросочетание в графе – это множество попарно непересекающихся
между собой ребер графа. Например, в графе (рис. 24) можно выделить паросочетание {x1, x2}, {x3, x5}. На рис. 25 ребра выделены более толстыми
линиями.
Рис. 25. Граф с выделенными ребрами паросочетания
Алгоритмов выделения паросочетания в графе достаточно много.
Рассмотрим один из самых простых и понятных алгоритмов выделе-
ния паросочетания в графе, который основан на следующем свойстве ребер
паросочетания.
Пусть какое-нибудь ребро u
1,2
между вершинами x1 и x2 вошло в па-
росочетание. Тогда все ребра инцидентные этим вершинам уже не могут
войти в паросочетание.
Алгоритм заключается в том, что необходимо определить ребро в
графе между двумя вершинами, еще не вошедшее в паросочетание, а затем
в графе нужно удалить все ребра, инцидентные этим двум вершинам графа.
Для удаления ребер, инцидентных вершине графа, необходимо в
двумерном массиве, моделирующем матрицу смежности, все элементы,
равные 1, в строке и столбце, соответствующие этой вершине, заменить на
значение 0.
Конечно, можно ребра и не удалять, а только помечать в матрице
смежности. Например, отрицательным значением, т.е. вместо 1, необходимо поставить минус 1. Тогда в матрице смежности при выборе очередного
ребра паросочетания, ребра, помеченные минус 1, выбирать в паросочетание будет уже нельзя.
x1
x2
x3
x4
x5

8.2. Программирование функции определения паросочетания графа
85
Рассмотрим пример определения паросочетания графа, показанного
на рис. 26.
Рис. 26. Граф контрольного примера
В соответствии с алгоритмом первым выбирается ребро {x1, x2} и из
графа удаляются ребра: {x1, x5}, {x2, x3}, {x2, x4}, {x2, x5} (рис. 27).
Тогда останутся только два ребра {x3, x5} и {x3, x4}, из которых
нужно выбрать следующее ребро в паросочетание.
Рис. 27. Граф с удаленными ребрами при выборе
в паросочетание ребра {x1, x2}
Из оставшихся ребер {x3, x4} и {x3, x5} на следующей итерации будет выбрано ребро {x3, x4} (рис. 28).
После чего будет удалено из графа ребро {x3, x5}. При дальнейшем
поиске по матрице смежности ребер обнаружено не будет. В результате будет выделено паросочетание {x1, x2}, {x3, x4} (рис. 29).
x1
x2
x3
x4
x5
x1
x2
x3
x4
x5

8. Программирование задач на графах повышенной сложности
86
Рис. 28. Граф контрольного примера при выборе в паросочетание ребра {x3, x4}
Рис. 29. Паросочетание в графе контрольного примера
Реализуем рассмотренный алгоритм выделения паросочетания графа
в функции task1() класса Task, которую разместим вне класса
Task:
void Task :: task1()
{
// циклы поиска ребра
for(int i=0;i<N;i++)
for(int j=0;j<N;j++)
if (matr[i][j]!=0)
{
// цикл удаление ребер
for(int k=0;k<N;k++)
{ matr[i][k]=0;
x1
x2
x3
x4
x5
x1
x2
x3
x4
x5

8.3. Программирование функции определения семейства клик, покрывающего…
87
matr[k][i]=0;
matr[j][k]=0;
matr[k][j]=0;
}
cout<<i+1<<" "<<j+1<<endl;
}
}
8.3. Программирование функции определения семейства клик,
покрывающего все вершины графа
Задача выделения клик графа является одной из наиболее интерес-
ных, так называемых, экстремальных задач на графах.
Клика графа – это максимальный полный подграф, который не вхо-
дит ни в один полный подграф более высокого порядка.
Задача определения всех клик графа является NP-трудной задачей.
Поэтому задачи выделения семейств клик графа, обладающих определенными свойствами, имеет большое практическое значение.
Семейство клик графа, покрывающее все вершины графа, относится
к задачам наиболее часто используемых на практике, поскольку позволяет
получить решение с учетом всех объектов, моделируемых графом.
Алгоритм определения семейства клик графа, покрывающего все
вершины графа, является итерационным и заключается в следующем.
На каждой итерации выбирается вершина графа xi, которая не вошла
ни в одну клику. На первой итерации такой вершиной может быть первая
вершина x1 или вершина, имеющая наибольшую (наименьшую) локальную
степень, или вершина, выбираемая по другим критериям.
Выделяется клика, в которую входит вершина xi. После этого помечаются все вершины, вошедшие в эту клику, как покрытые вершины.
Затем переходят к следующей итерации. Процесс продолжается до
тех пор, пока не будет ни одной непокрытой семейством клик вершины.
При программировании такого алгоритма определения семейства
клик, покрывающего все вершины графа, кроме двумерного массива, моделирующего матрицу смежности графа, объявляют одномерный вспомогательный массив, размерность которого равна количеству вершин графа. До
первой итерации каждому элементу этого массива присваивается нулевое

8. Программирование задач на графах повышенной сложности
88
значение, что будет означать, что вершина не вошла ни в одну клику. Если
вершина войдет в клику, то элементу этого массива, номер которого равен
номеру вершины графа, вошедшей в клику, присваивается значение 1, что
и будет означать, что вершина уже вошла в какую-то клику, т.е. покрыта
семейством клик.
Аналогично выделяется семейство клик, покрывающее все ребра
графа, с той лишь разницей, что выбираются непокрытые ребра графа, а
покрытые ребра графа помечаются присвоением элементам матрицы смежности, например, отрицательного значения.
Рассмотрим программирование функции определения семейства клик
графа, покрывающего все вершины графа, – функции task2() класса
Task.
Выделим память под укзазатели на вспомогательные массивы:
int *P=0, *Cl=0;
bool *M=0;
Создадим эти вспомогательные массивы в динамической памяти:
M = new bool[N];
P = new int [N];
Cl = new int [N];
Массив P предначначен для пометки вершин графа, которые покры-
ты выделенными кликами графа. Перед выделением семейства клик присвоим всем элемента массива P нулевые значения, показав, тем самым, что
ни одна вершина непокрыта, т.е. не вошла пока ни в одну клику графа:
for (int i=0; i<N; i++) P[i]=0;
Затем организуем цикл просмотра массива P:
for (int i=0; i<N; i++) { … }
для того, чтобы найти непокрытую кликами вершину, т.е. вершину, которой в массиве P будет соответвовать элемент с нулевым значением:

8.3. Программирование функции определения семейства клик, покрывающего…
89
for (int i=0; i<N; i++)
{
z=0;
if(P[i]==0){ … }
}
Если такая вершина найдена, то индекс i такого элемента будет ра-
вен индексу строки матрицы, соответствующей непокрытой вершине.
Номер этой вершины запоминаем в элементе массива Cl[z], где
z – индекс очередного элемента массива Cl, в который нужно записать
номер новой вершины выделяемой клики графа и увеличить значение z на
единицу:
Cl[z]=i+1; z=z+1;
Теперь нужно организовать процедуру выделения клики, которая
обязательно содержит вершину с номером i+1.
Копируем строку матрицы во вспомогательный массив M:
for(int j=0; j<N; j++)
M[j]= matr[i][j];
Затем организуем цикл, на каждой итерации которого находим в мас-
сиве M очередной элемент M[j]==1 и находим логическое произведение элементов массива M и сроки матрицы с индексом j, соответвующим
элементу M[j]==1. Результат такого произведения записываем опять в
тот же массив M:
for (int j=0; j<N; j++)
if(M[j]==1)
{
Cl[z++]=j+1;
cout<<" nomer="<<z<<' '<<Cl[z-1];
for(int l=j+1; l<N; l++)
M[l]= M[l]*matr[j][l];
}

8. Программирование задач на графах повышенной сложности
90
Цикл завершится, когда в массиве M будут рассмотрены все N эле-
ментов, после чего в массиве Cl будут сформированы номера вершин
выдленной клики, в котору входит вершина с номером i+1.
После этого помечаем в массиве P все вершины, номера которых
сформированы в массиве Cl:
for(int i1=0; i1<z; i1++)
P[Cl[i1]-1]=1;
и выводим на экран номера вершин, выделенной клики, т.е. значения элементов массива Cl.
Функцию определения семейства клик, покрывающего все вершины
графа, – функцию task2() класса Task, разместим вне класса Task:
void Task :: task2()
{
int *P=0, *Cl=0;
bool *M=0;
int z=0;
M = new bool[N];
P = new int [N];
Cl = new int [N];
//---- обнуление массива флажков
for (int i=0; i<N; i++)
P[i]=0;
//--- выделение клики графа для непокрытой вершины
for (int i=0; i<N; i++)
{
z=0;
if(P[i]==0)
{
Cl[z]=i+1; // запись номера вершины клики
z=z+1;
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
