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

книги / Математическая логика и теория алгоритмов. Анализ алгоритмов

.pdf
Скачиваний:
3
Добавлен:
12.11.2023
Размер:
8.87 Mб
Скачать

ПРИЛОЖЕНИЕ А

Пример отчета по практической работе «Исследование и сравнение сложности алгоритмов»

Федеральное государственное бюджетное образовательное учреждение высшего образования «Пермский национальный исследовательский политехнический университет»

ОТЧЕТ

по практической работе

“Исследование и сравнение сложности алгоритмов”

Работу выполнили студенты группы КОБ-18 Иванов И.И.

“_____” 20__ г.

Пермь 2020

151

1. Описание задачи

Задача о вершинном покрытии минимальной мощности

Вершинное покрытие для неориентированного графа G = (V, E) – это подмножество его вершин S такое, что у каждого ребра графа хотя бы один из концов принадлежит S.

Размером вершинного покрытия называется число входящих в него вершин.

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

Вход: граф G.

Результат: k – размер наименьшего вершинного покрытия S графа G. Результатом является пара (k, S).

Пример:

Рисунок 1 – Пример графа

Для приведенного графа есть, например, вершинное покрытие {1, 3, 5, 6}, но оно не является минимальным по размеру. Минимальным покрытием в данном примере является множество вершин {2, 4, 5}, соответственно его размер равен 3.

2. Формат входных данных

На вход алгоритму подаются число N – количество вершин в графе и квадратная матрица u[N][N] – матрица смежности данного графа.

Для графа, приведенного на рис. 1, входные данные будут такими: N = 6;

u[6][6] =

152

 

1

2

3

4

5

6

1

0

1

0

0

1

0

2

1

0

1

0

1

0

3

0

1

0

1

0

0

4

0

0

1

0

1

1

5

1

1

0

1

0

0

6

0

0

0

1

0

0

3. Краткое описание реализованных алгоритмов.

1) Алгоритм полного перебора.

Идея данного алгоритма заключается в том, чтобы перебрать все подмножества множества вершин данного графа.

Для начала будем считать текущим минимальным размером вершинного покрытия число N, так как все вершины графа

точно образуют вершинное покрытие: kmin N, Smin ← {1, …, N}. Предположим, что мы выбрали какое-либо подмножество S. То-

гда проверим, является ли S корректным вершинным покрытием. Если оно является таковым, сравним размер S с текущим минимальным размером kmin. Если размер S < kmin, то обновим оптимальные значения: kmin ← размер S, Smin S. Алгоритм заканчивается, когда будут перебраны все возможные подмножества. Ответом является пара (kmin; Smin).

2) Приближённый алгоритм.

Идея алгоритма в следующем: давайте возьмём случайное ребро (u, v) в данном графе и скажем, что вершины u и v будут входить в вершинное покрытие. Удалим из графа все рёбра, инцидентные вершинам u и v. Будем повторять данную операцию до тех пор, пока у нас не останется ни одного ребра в графе. Полученное множество вершин будет являться ответом. Данный алгоритм является 2-оптимальным, то есть ответ данного алгоритма (размер найденного вершинного покрытия) больше оптимального не более чем в два раза.

3) Жадный алгоритм Алгоритм является итерационным. На каждой итерации

будем выбирать вершину, степень которой в графе максимальна. После данная вершина добавляется в вершинное покрытие и уда-

153

ляется из графа (вместе со всеми инцидентными ей рёбрами). Алгоритм завершается, когда в графе не остаётся ни одного ребра.

4. Созданная программа.

Исходный код программы приведен в приложении А. 5. Теоретические оценки сложности.

Подсчет количества выполняемых операций для определения функций сложности указан в виде комментариев в тексте программы в приложении А. Отметим здесь асимптотические оценки порядкаростафункцийсложностиреализованныхалгоритмов.

Алгоритм полного перебора

Происходитперебор всех подмножеств (их количество – 2N), при этом для каждого подмножества выполняется проверка, что каждое ребро (они перебираются по матрице смежности, поэтому количество итераций равно N2) удовлетворяет условию: хотя бы один из концов принадлежит рассматриваемому подмножеству (данная проверка происходит за фиксированное количество операций благодаря заранее подготовленной структуре данных). Таким образом, асимптотическая оценка сложности алгоритма составляет O (N2∙2N).

Приближённый алгоритм

На каждой итерации алгоритма выполняется поиск по матрице смежности какого-либо еще не удаленного ребра, что требует порядка N2 операций. Количество итераций совпадает с размером искомого вершинного покрытия, который, очевидно, не превышает N. Таким образом, асимптотическая оценка сложности алгоритма составляет O (N3).

Жадный алгоритм

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

154

операций. Количество итераций совпадает с размером искомого вершинного покрытия, который, очевидно, не превышает N.

Таким образом асимптотическая оценка сложности алгоритма составляет O (N3).

6.Использованный язык программирования и среда разработки.

Для разработки данных алгоритмов был выбран язык C++14, так как является одним из наиболее оптимальных языков для реализации алгоритмических функций.

Была выбрана среда code:: blocks 16.01, так как она является легковесной и для данной задачи обеспечит наиболее быструю разработку.

7.Характеристики оборудования.

Процессор – 8x Intel (R) Core (TM) i7-4700MQ.

Тактовая частота – 2.40 GHz. Оперативная память – 12232MB.

ОС – Ubuntu 16.04.2 LTS.

8. Определение максимального объема данных.

После проверки работы всех реализованных алгоритмов на данных различных объемов было определено, что самым медленным алгоритмом является алгоритм полного перебора. Максимальный объём данных, на котором данный алгоритм работает за приемлемое время, – количество вершин в графе N = 27. На данном объеме программа отрабатывает за 127,7 секунд.

9. Графики зависимости работы программы от объема входных данных.

Экспериментально установлено, что время выполнения одной элементарной операции составляет приблизительно 2.52773417151∙10–10 секунд. С учетом этого построены графики зависимости времени работы программы от размера входных данных (на графиках синим цветом отображены теоретические оценки, коричневым – экспериментальные).

На основе данных графиков можно заметить, что практические и теоретические оценки времени работы почти совпадают.

155

Рисунок 2 – Сложность переборного алгоритма

Рисунок 3 – Сложность приближенного алгоритма

Рисунок 4 – Сложность «жадного» алгоритма

156

10. Характеристики работы различных алгоритмов. Время работы и отклонения приближенных решений от

точных указаны в таблице.

Характеристики алгоритмов

 

Переборный,

Приближенный

 

Жадный

 

N

время

процент

среднее

время

процент

среднее

время

работы,

коррект-

отклоне-

работы,

коррект-

отклоне-

 

работы,с

ныхотве-

ныхотве-

 

 

с

тов

ние

с

тов

ние

 

 

 

 

0,0001

 

5

0,001

0,0001

28,571%

52,381%

100%

0%

10

0,001

0,0001

14,28%

39,807%

0,0001

100%

0%

15

0,023

0,0001

21,429%

33,371%

0,0001

71,429%

2,832%

18

0,223

0,0001

7,143%

46,055%

0,0001

92,857%

7,143%

20

1,029

0,0001

7,143%

51,222%

0,0001

71,429%

8,859%

21

2,309

0,0001

14,28%

40,653%

0,0001

64,286%

9,234%

27

127,7

0,0002

7,143%

55,763%

0,0002

64,286%

8,859%

Вреализованной программе была описана функция проверки на корректность ограничения 2-оптимальности для приближённого алгоритма. На всех группах тестов программа выдала ответ хуже оптимального не более чем в два раза, следовательно, свойство 2-оптимальности выполнено.

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

Тест против «жадного» алгоритма.

Вприведённом на рисунке 5 графе покрывающее множество минимальной мощности будет состоять из 12 вершин, образующих группу C.

Изначально у вершин из C и N3 по 4 выходящих ребра,

увершин из N4 – по3, у вершин из N6 – по2 и у вершин из N12 – по одному, поэтому «жадный» алгоритм на первых трёх шагах может выбрать вершины из N3. После этого у вершин из C останется только по 3 ребра, а это столько же, сколько и у вершин

157

из N4, поэтому на следующих 4 шагах «жадное» решение может выбрать вершины из N4. Далее точно так же оно выберет вершины из N6 и N12, получив в итоге покрывающее множество из 25 вершин вместо 12.

Рисунок 5 – Структура графа для тестирования «жадного» алгоритма

Заключение В ходе данной работы были реализованы 3 различных ал-

горитма решения задачи о вершинном покрытии минимальной мощности.

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

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

Однако если необходимо обеспечить ограничение на количество вершин относительно оптимального ответа, то следует использовать приближенный алгоритм, так как он обладает свойством 2-оптимальности. Жадный алгоритм имеет тесты определенных видов, на которых размер вершинного покрытия, которое находит жадный алгоритм, больше чем в 2 раза по сравнению с оптимальным ответом.

158

ПРИЛОЖЕНИЕ Б

Пример оформления исходного кода программы

#include <bits/stdc++.h> using namespace std;

ostream &operator << (ostream &fo, const vector<int> &v) { fo << v.size() << ": ";

for (int i = 0; i < (int)v.size(); i++) fo << v[i] << " ";

return fo;

}

const int N = 1000; bool u[N][N];

int n;

void readData() { cin >> n;

int x;

for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) {

cin >> x; u[i][j] = (bool)x;

}

}

}

vector<int> bruteForceSolution() { ///T = O(2^N * N^2) if (n > 27) return {-1}; ///+2

vector<int> ans, cur; ///+0

int ans_sz = n + 1, cur_sz; ///+2

bool sel[N]; ///+0

int mx = (1 << n); ///+2

for (int mask = 0; mask < mx; mask++) { ///2^n масок - 2 + 2 * 2^n операций шапки цикла

159

cur.clear(); ///очистка за количество элементов, в среднем n/2 элементов - n * 2^n / 2

for (int i = 0; i < n; i++) { ///2^n * (2 + 2 * n)

if (mask & (1 << i)) { ///будет выполнено суммарно

2^n*n/2 операций - n * 2^n

cur.push_back(i + 1); /// в данной ветке 4 операции sel[i] = true;

} else {

sel[i] = false; /// в данной ветке 2 операции

}

///каждая изданныхветоквыполнитсяодинаковоеколичествораз,поэтому будетвыполнено(4+2)/2*n*2^nопераций

}

cur_sz = cur.size(); ///2^n

bool fail = false; ///2^n

for (int i = 0; i < n; i++) ///2^n*(2 + 2 * n)

for (int j = i; j < n; j++) ///формула для количества данных операций 2^n * (2 + 3 + ... + n + 1) = 2^n * ((n + 1) * (n + 2) / 2 - 1) if (u[i][j] && !sel[i] && !sel[j]) /// 8 * 2^n * (1 + 2 + ... +

n - 1) = 4* 2^n * n * (n - 1)

fail = true; /// так как тесты рандомизированные, то вероятность выполнения условия - 1/2, тогда количество С1 * 2^n * (1 + 2 + ... + n - 1)

if (!fail && cur_sz < ans_sz) { ///2^n*3

ans_sz = cur_sz; ///условие выполниться не более n раз,

тогда C2 * n * 2 ans = cur;

}

}

return ans; ///+1

///2+2+2+2 + 2*2^n + n*2^n/2 + 2^n*(2+2n) + n*2^n + 3/2*n*2^n + 2^n + 2^n + 2^n*(2+2*n) + 2^n*(n^2/2+3n/2) + 4*n*(n-1)*2^n + C1*2^n*n*(n-1) + 2^n*3 + C2*2*n + 1

}

160