Uploaded by Oleg Petrov

Алгоритмы дискретной математики

advertisement
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное государственное бюджетное образовательное учреждение высшего образования
«САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ МОРСКОЙ ТЕХНИЧЕСКИЙ
УНИВЕРСИТЕТ»
Кафедра прикладной математики и математического моделирования
Курсовая работа
по дисциплине «Алгоритмы дискретной математики»
на тему
«Сортировки. Графы»
Выполнил:
Студент группы 1200
Петров О.Ю.
Проверил(а):
доцент Войткунская А.Я.
……………………….…………..… (оценка)
……………………….………………. (дата)
Санкт-Петербург
2017 г.
СОДЕРЖАНИЕ
ВВЕДЕНИЕ .................................................................................................................................... 2
1. ПОСТАНОВКА ЗАДАЧИ ........................................................................................................ 2
2. АЛГОРИТМ ............................................................................................................................... 2
2.1 Оценка сложности алгоритма ................................................................................................ 3
3. ПРОГРАММНАЯ ДОКУМЕНТАЦИЯ ................................................................................... 4
4. ТЕСТИРОВАНИЕ ПРОГРАММЫ .......................................................................................... 5
1.
Постановка задачи ................................................................................................................. 7
1.1 Условие задачи ........................................................................................................................ 7
1.2 Анализ и уточнение условия .................................................................................................. 7
2.
Алгоритм................................................................................................................................. 7
3.
Программная документация ............................................................................................... 10
4. ТЕСТИРОВАНИЕ ПРОГРАММЫ ........................................................................................ 14
ЗАКЛЮЧЕНИЕ ........................................................................................................................... 15
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ .................................................................. 16
1
ВВЕДЕНИЕ
Работа выполнена в рамках учебной дисциплины «Алгоритмы дискретной
математики», изучаемой студентами специальности «Прикладная математика». В
ней рассматриваются вопросы сортировки массивов на примере массива из целых
чисел, а также алгоритм проверки двудольности графа, представленного в виде
массива векторов. Цель работы – изучение приемов создания и обработки массивов,
разработка и отладка программы на языке С++, реализующей эти приемы.
При создании программы я использовал пользовательский интерфейс
популярного компилятора для ОС Windows - Visual Studio. Для начала работы я
должен был создать проект (Вкладка Файл / Создать / Проект), при создании указать,
что моя программа будет реализована в коде C++. Далее, в свою очередь, в проекте
необходимо создать основной файл, в который помещается код программы
(описание функций, переменных и т.д и действия над ними).
После того, как код был написан, я запускаю компилятор программы (Вкладка
Отладка / Начать отладку). Появляется черное окно в которое мне следует ввести
необходимые данные (либо вручную; либо они вводятся автоматически из
текстового файла, заранее помещенного в мой проект). Программа выдает результат,
после чего черное окно можно закрыть.
ЧАСТЬ 1
1. ПОСТАНОВКА ЗАДАЧИ
Дан массив из целых чисел. Его необходимо сортировать с помощью быстрой
сортировки по возрастанию. Целые числа - это ключи, за которыми скрываются
записи о каждом элементе.
2. АЛГОРИТМ
Алгоритм или общий принцип быстрой сортировки представляет собой 3
этапа:
a. Выбор стартового элемента массива. В моем случае был выбран
центральный элемент.
b. Разбиение: перераспределение элементов в массиве таким образом, что
элементы меньше стартового помещаются перед ним, а больше или
равные после.
2
c. Рекурсивно применить первые два шага к двум подмассивам слева и
справа от опорного элемента. Рекурсия не применяется к массиву, в
котором только один или отсутствуют элементы.
2.1 ОЦЕНКА СЛОЖНОСТИ АЛГОРИТМА
Ясно, что операция разделения массива на две части относительно опорного
элемента занимает время O(n). Поскольку все операции разделения, проделываемые
на одной глубине рекурсии, обрабатывают разные части исходного массива, размер
которого постоянен, суммарно на каждом уровне рекурсии потребуется также O(n)
операций. Следовательно, общая сложность алгоритма определяется лишь
количеством разделений, то есть глубиной рекурсии. Глубина рекурсии, в свою
очередь, зависит от сочетания входных данных и способа определения опорного
элемента.
Лучший случай:
В наиболее сбалансированном варианте при каждой операции разделения
массив делится на две почти одинаковые части, следовательно, максимальная
глубина рекурсии, при которой размеры обрабатываемых подмассивов достигнут 1,
составит log(2) n. В результате количество сравнений, совершаемых быстрой
сортировкой, было бы равно значению рекурсивного выражения C(n)=2*С(n/2)+n,
что даёт общую сложность алгоритма O(n*log(2) n).
Среднее:
Среднюю сложность при случайном распределении входных данных можно
оценить лишь вероятностно.
Прежде всего необходимо заметить, что в действительности необязательно,
чтобы опорный элемент всякий раз делил массив на две одинаковых части.
Например, если на каждом этапе будет происходить разделение на массивы длиной
75 % и 25 % от исходного, глубина рекурсии будет равна log(4/3) n, а это попрежнему даёт сложность O(n*log n). Вообще, при любом фиксированном
соотношении между левой и правой частями разделения сложность алгоритма будет
той же, только с разными константами.
Будем считать «удачным» разделением такое, при котором опорный элемент
окажется среди центральных 50 % элементов разделяемой части массива; ясно,
вероятность удачи при случайном распределении элементов составляет 0,5. При
удачном разделении размеры выделенных подмассивов составят не менее 25 % и не
более 75 % от исходного. Поскольку каждый выделенный подмассив также будет
иметь случайное распределение, все эти рассуждения применимы к любому этапу
сортировки и любому исходному фрагменту массива.
Удачное разделение даёт глубину рекурсии не более log(4/3) n. Поскольку
вероятность удачи равна 0,5, для получения k удачных разделений в среднем
потребуется k*2 рекурсивных вызовов, чтобы опорный элемент k раз оказался среди
центральных 50 % массива. Применяя эти соображения, можно заключить, что в
3
среднем глубина рекурсии не превысит 2*log(4/3) n, что равно O(log n) А поскольку
на каждом уровне рекурсии по-прежнему выполняется не более O(n) операций,
средняя сложность составит O(n*log n).
Худший случай:
В самом несбалансированном варианте каждое разделение даёт два
подмассива размерами 1 и n-1, то есть при каждом рекурсивном вызове больший
массив будет на 1 короче, чем в предыдущий раз. Такое может произойти, если в
качестве опорного на каждом этапе будет выбран элемент либо наименьший, либо
наибольший из всех обрабатываемых. При простейшем выборе опорного элемента
— первого или последнего в массиве, — такой эффект даст уже отсортированный (в
прямом или обратном порядке) массив, для среднего или любого другого
фиксированного элемента «массив худшего случая» также может быть специально
подобран. В этом случае потребуется n-1 операций разделения, а общее время
работы составит sum (0...n) n-i=O(n^2) операций, то есть сортировка будет
выполняться за квадратичное время. Но количество обменов и, соответственно,
время работы — это не самый большой его недостаток. Хуже то, что в таком случае
глубина рекурсии при выполнении алгоритма достигнет n, что будет означать nкратное сохранение адреса возврата и локальных переменных процедуры
разделения массивов. Для больших значений n худший случай может привести к
исчерпанию памяти (переполнению стека) во время работы программы.
3. ПРОГРАММНАЯ ДОКУМЕНТАЦИЯ
Для начала подключаем заголовочный файл iostream для ввода-вывода и
cstdlib для генерации случайных чисел. Объявляем пространство имен стандартным.
И создаем глобальный массив из целых элементов.
Далее объявляем и описываем саму функцию быстрой сортировки. В
формальных параметрах у этой функции стоят: l - начальный элемент сортировки r
- конечный элемент сортировки. x - ключ, т.е. значение того элемента, который был
принят за стартовый. i, j - ползунки, их используют для движения по массиву.
Устанавливаем ползунки в крайние положения. После сортируем массив
относительно среднего элемента. Далее рекурсивно сортируем тот подмассив,
который находится до среднего элемента. И так продолжаем, до тех пор, пока он не
отсортируется полностью. Производим те же действия со вторым подмассивом.
4
В главной функции определяем количество элементов в массиве. Элементы
вводятся автоматически с помощью генератора псевдослучайных чисел (фунции
rand). В моем случае это случайные числа от 0 до 999. Выводим на экран исходный
массив. Сортируем его и печатаем результат.
4. ТЕСТИРОВАНИЕ ПРОГРАММЫ
Пример 1 (10 элементов):
Пример 2 (100 элементов):
5
6
ЧАСТЬ 2
1. ПОСТАНОВКА ЗАДАЧИ
1.1 УСЛОВИЕ ЗАДАЧИ
Дан граф в виде двумерного массива. Написать алгоритм проверки
двудольности графа. Протестировать программу на следующих тест примерах:
1. Граф порядка 10
2. Граф порядка 500
3. Граф, не являющийся двудольным
1.2 АНАЛИЗ И УТОЧНЕНИЕ УСЛОВИЯ
Очень важно иметь представление о том, какой граф является двудольным.
Граф называют двудольным, если множество его вершин разбивается на два
подмножества, таких что нет ребер имеющих обе концевые вершины в одном и том
же подмножестве. Пример двудольного графа представлен на рисунке.
2. АЛГОРИТМ
Граф представляет собой совокупность ребер и вершин. Ребра графа
представлены в виде двумерного вектора edges из m векторов по три элемента в
каждом. Первый элемент — это первая вершина конкретного ребра, второй —
вторая, а третий — это «цвет» ребра, он может иметь значение 0 или 1. Совокупность
вершин — это одномерный вектор nodes из n элементов, в котором последовательно
7
записаны все вершины, входящие в граф. Для проверки нужны два дополнительных
вектора u и v.
Для начала нужно создать граф, то есть задать количество вершин n, и
последовательно их ввести, потом задать количество ребер m и ввести эти ребра
(либо вручную с помощью функции manual_input, либо автоматически при помощи
функции automaticly_input).
Далее управление передается функции test, которая как раз и проверяет граф
на двудольность.
void test(vector<vector<int>> edges, vector <int> nodes, int m, int n)
{
bool result = true;
vector<int> u(125250), v(125250);
int i, j, k, count = 0;
for (i = 0; i < n; i++)
for (j = 0; j < m;j++)
{
if (nodes[i] == edges[j][0] && edges[j][2] == 0)
{
for (k = 0; k <= count;k++)
{
if (nodes[i] == u[k])
{
u[count] = edges[j][0];
v[count] = edges[j][1];
edges[j][2] = 1;
count++;
break;
}
else if (nodes[i] == v[k])
{
u[count] = edges[j][1];
v[count] = edges[j][0];
edges[j][2] = 1;
count++;
break;
}
else
{
u[count] = edges[j][0];
v[count] = edges[j][1];
edges[j][2] = 1;
count++;
break;
}
8
}
}
else if (nodes[i] == edges[j][1] && edges[j][2] == 0)
{
for (k = 0; k <= count;k++)
{
if (nodes[i] == u[k])
{
u[count] = edges[j][1];
v[count] = edges[j][0];
edges[j][2] = 1;
count++;
break;
}
else if (nodes[i] == v[k])
{
u[count] = edges[j][0];
v[count] = edges[j][1];
edges[j][2] = 1;
count++;
break;
}
else
{
u[count] = edges[j][1];
v[count] = edges[j][0];
edges[j][2] = 1;
count++;
break;
}
}
}
}
for (i = 0;i < m;i++)
for (j = 0; j < m; j++)
{
if (u[i] == v[j])
result = false;
}
if (result == false)
cout << "Граф не двудольный!" << endl;
if (result == true)
cout << "Граф двудольный!" << endl;
}
9
Если после выполнения функции test в u и v встречаются одинаковые номера
вершин, тогда граф не двудольный. Если одинаковых вершин в u и v нет, тогда
двудольный.
3. ПРОГРАММНАЯ ДОКУМЕНТАЦИЯ
#include <cstdlib>
#include <ctime>
#include <vector>
#include <iostream>
using namespace std;
void print_edges(vector <vector <int>>, int); //функция для вывода на экран ребер
vector<vector<int>> manual_input(vector<vector<int>> edges, int m); //функция для
инициализации пар чисел (ребер)
vector<vector<int>> automaticly_input(vector<vector<int>>, vector <int>, int, int);
//фунция для автоматической инициализации пар чисел (граф всегда двудольный)
void test(vector<vector<int>>, vector <int>, int, int); //функция проверки графа на
двудольность
void main()
{
int i, m, n, tmp;
vector <vector <int>> edges(125250, vector<int>(3)); //двумерный вектор
(ребра)
vector <int> nodes(501); //вектор (вершины)
setlocale(LC_ALL, "Russian");
cout << "Введите количество вершин графа: ";
cin >> n;
for (i = 0; i < n; i++)
nodes[i] = i + 1;
cout << "Введите количество ребер графа: ";
cin >> m;
cout << "Хотите ввести граф вручную(1) или автоматически(0)?" << endl;
cin >> tmp;
switch (tmp)
{
case 0:
{
edges = automaticly_input(edges, nodes, m, n);
break;
}
case 1:
edges = manual_input(edges, m);
10
break;
}
print_edges(edges, m);
test(edges, nodes, m, n);
system("pause");
}
void print_edges(vector<vector<int>> vec, int m)
{
for (int i = 0; i < m;i++)
{
cout << "Ребро №" << i + 1 << ": " << vec[i][0] << " " << vec[i][1] <<
endl;
}
}
vector<vector<int>> manual_input(vector<vector<int>> edges, int m)
{
cout << "Введите ребра в виде пары чисел (ребра неориентированные): " <<
endl;
for (int i = 0; i < m;i++)
{
cin >> edges[i][0] >> edges[i][1];
edges[i][2] = 0;
}
return edges;
}
vector<vector<int>> automaticly_input(vector<vector<int>> edges, vector <int> nodes,
int m, int n)
{
srand(time(NULL));
for (int i = 0; i < m; i++)
{
int x = rand() % 2;
switch (x)
{
case 0:
{
edges[i][0] = nodes[rand() % (n / 2)];
edges[i][1] = nodes[rand() % (n / 2) + (n / 2)];
edges[i][2] = 0;
break;
}
case 1:
{
11
edges[i][1] = nodes[rand() % (n / 2)];
edges[i][0] = nodes[rand() % (n / 2) + (n / 2)];
edges[i][2] = 0;
break;
}
}
}
return edges;
}
void test(vector<vector<int>> edges, vector <int> nodes, int m, int n)
{
bool result = true;
vector<int> u(125250), v(125250);
int i, j, k, count = 0;
for (i = 0; i < n; i++)
for (j = 0; j < m;j++)
{
if (nodes[i] == edges[j][0] && edges[j][2] == 0)
{
for (k = 0; k <= count;k++)
{
if (nodes[i] == u[k])
{
u[count] = edges[j][0];
v[count] = edges[j][1];
edges[j][2] = 1;
count++;
break;
}
else if (nodes[i] == v[k])
{
u[count] = edges[j][1];
v[count] = edges[j][0];
edges[j][2] = 1;
count++;
break;
}
else
{
u[count] = edges[j][0];
v[count] = edges[j][1];
edges[j][2] = 1;
count++;
break;
}
12
}
}
else if (nodes[i] == edges[j][1] && edges[j][2] == 0)
{
for (k = 0; k <= count;k++)
{
if (nodes[i] == u[k])
{
u[count] = edges[j][1];
v[count] = edges[j][0];
edges[j][2] = 1;
count++;
break;
}
else if (nodes[i] == v[k])
{
u[count] = edges[j][0];
v[count] = edges[j][1];
edges[j][2] = 1;
count++;
break;
}
else
{
u[count] = edges[j][1];
v[count] = edges[j][0];
edges[j][2] = 1;
count++;
break;
}
}
}
}
for (i = 0;i < m;i++)
for (j = 0; j < m; j++)
{
if (u[i] == v[j])
result = false;
}
if (result == false)
cout << "Граф не двудольный!" << endl;
if (result == true)
cout << "Граф двудольный!" << endl;
}
13
4. ТЕСТИРОВАНИЕ ПРОГРАММЫ
Пример 1 (10 элементов):
Пример 2 (500 элементов):
Пример 3 (граф, не являющийся двудольным):
14
ЗАКЛЮЧЕНИЕ
Поставленные задачи были полностью решены. Результатом решений явились
отлаженные программы на языке С++. Правильность работы программ
подтверждена проведенным тестированием.
15
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
1. Павловская Т.А. С/С++. Программирование на языке высокого уровня. СПб.:
Питер, 2013.
2. Р. Седжвик Алгоритмы на С++.
3. Чурилов А.Н. Конспект лекций по дисциплине «Программирование и
алгоритмические языки». СПбГМТУ, 2017.
4. Сайт ru.wikipedia.org/wiki/Быстрая_сортировка.
5. Сайт cybern.ru/qsort-cpp.html.
16
Download