Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы объектно-ориентированного программирования задач на графах. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
1 Мб
Скачать
☆
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 определим чистые виртуальные функции in­put_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;
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]