Оценка временной сложности некоторых классов алгоритмов

Понятие алгоритма и анализ теоретических оценок временной сложности алгоритмов умножения матриц. Сравнительный анализ оценки временной сложности некоторых классов алгоритмов обычным программированием и программированием с помощью технологии Open MP.

Рубрика Программирование, компьютеры и кибернетика
Вид дипломная работа
Язык русский
Дата добавления 12.08.2017
Размер файла 1,6 M

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

Размещено на http://www.allbest.ru/

Содержание

временный сложность алгоритм программирование

Введение

1. Обзор литературы

2. Определение временной сложности алгоритма и важность выбора технологии программирования при реализации решаемых задач

2.1 Понятия алгоритма

2.2 Понятие оценки временной сложности алгоритма

2.3 Пример важности выбора технологии программирования при реализации задач

3. Теоретические оценки временной сложности алгоритмов умножения матриц

3.1 Алгоритм простого умножения матриц

3.2 Алгоритма Штрассена

3.3 Основные понятия алгоритма сортировки

3.4 Сортировка пузырьком

3.5 Сортировка вставками

3.6 Быстрая сортировка

4. Основные теоретические положения технологии параллельного программирования Open MP

4.1 Введение

4.2 Классификация вычислительных машин

4.3 Мультипроцессоры

4.4 Технология ОреnMР

4.5 Основные понятия технологии ОреnMР

4.5.1 Компиляция программы

4.5.2 Модель параллельной программы

4.5.3 Директивы и функции

4.5.4 Выполнение программы

5. Оценка временной сложности некоторых классов алгоритмов с помощью последовательного программирования

5.1 Алгоритмы умножения матриц

5.2 Алгоритмы сортировок одномерных массивов

6. Сравнительный анализ оценки временной сложности некоторых классов алгоритмов обычным программированием и программированием с помощью технологии Open MP

7. Оценка временной сложности некоторых классов алгоритма с помощью технологии параллельного программирования

7.1 Алгоритм пузырьковой сортировки

7.2 Алгоритм сортировки вставок

7.3 Алгоритм быстрой сортировки

7.4 Алгоритм простого умножения матриц

Заключение

Список использованных источников

Приложение А

Приложение Б

Приложение В

Приложение Г

Введение

Актуальность темы.

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

Цель данной работы:

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

Задачи работы:

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

Научная новизна работы:

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

Практическая значимость работы:

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

Апробация работы.

Основные результаты работы изложены в трех статьях.

Первая статья «Численные эксперименты по оценке временной сложности некоторых алгоритмов» была публикована в интернет-журнале «Apriori».

Вторая статья - «Сравнительный анализ оценки временной сложности некоторых классов алгоритмов обычным программированием и программированием с помощью технологии ОРЕN MР» и доклад по ней уже признали лучшей в секции «Информационные системы и технологии» на межрегиональной научной конференции аспирантов и молодых ученых в номинации «Студенты».

В третьей статье «Оценка временной сложности алгоритмов с помощью параллельного программирования» была представлена на молодежном научном форуме «Молодые исследователи регионам».

Выпускная квалификационная работа состоит из семи разделов.

В первом разделе будет проведен обзор литературы по данной тематике.

Во втором разделе будут рассмотрены основные теоретические положения временной сложности алгоритмов, которые могут пригодятся на практике.

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

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

В пятом разделе будут доказаны теоретические оценки временной сложности алгоритмов с помощью последовательного программирования. В разделе рассматриваются примеры алгоритмов простого умножения матриц, блочного умножения, умножения матриц методом Штрассена. Будет проведен сравнительный анализ времени выполнения этих методов, в ходе которого будут сделаны выводы о самом эффективном и быстром алгоритме. Также в разделе будут рассмотрены алгоритмы сортировки одномерных массивов, в частности методы вставок, пузырьковой и быстрой сортировки. Информации из этой части взяты со статьи «Численные эксперименты по оценке временной сложности некоторых алгоритмов», опубликованной в интернет-журнале «Apriori».

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

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

В заключении будут сделаны выводы по проведенной научной исследовательской работе и доказана её актуальность.

1. Обзор литературы

Существует множество статей и диссертацией, совпадающих с тематикой данной работы. Рассмотрим некоторые из них.

Статья, опубликованная в электронном интернет - журнале “Арriоri”, кандидатом технических наук Самуйловым Сергеем Владимировичем «О практической необходимости определения сложности алгоритмов» повествует нам о понятиях сложности алгоритмов и необходимости её определения и анализа на начальных этапах решения определенной задачи. Автор научной статьи говорит о значимости и актуальности своей работы, доказывая данные суждения с помощью теории и практическими примерами. Делается вывод о том, что приведенные в статье примеры демонстрируют необходимость поиска для решения определенной задачи именно эффективного, а не наиболее очевидного или простого в программировании [1].

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

В статье В.О. Борознова «Исследование задачи коммивояжера» автор берет несколько методов решения определенной задачи, в данном случае - это задача коммивояжера, и сравнивает их не только по быстродействию, но и по качеству и точности. В заключении говорится о широком спектре применения методов решения задачи - в зависимости от потребности можно использовать тот или иной метод. Временные оценки, данные алгоритмам, позволяют оценить время решения задачи и выбрать наиболее подходящий метод [2].

В статье В.И. Шикаренко «Зависимость временной эффективности алгоритмов и программ обработки больших объемов данных от их кэширования» акцент делается на техническую составляющую. Автор берет несколько методов алгоритма сортировки и делает эксперименты с несколькими компьютерами на базе процессоров Intеl и АMD. Делаются выводы о том, что семейства ЭВМ на базе процессора Intеl и его аналогов являются существенно нелинейно прогнозируемыми по времени выполнения программ; вероятностных оценок вычислительной сложности недостаточно для принятия решений о выборе алгоритма из числа альтернативных для их включения в состав конкретного ПО [3].

С последним выводом можно согласиться. Действительно, для того, чтобы реализация алгоритма была эффективна, нужно не только пользоваться теоретическими основами сложностей алгоритмов. Статья полезна и при выборе персонального компьютера для решения каких-либо задач. Но в отличие от этой диссертационной работы, в научной работе В.И. Шикаренко не было акцентировано внимание на технологии программирования.

В магистерской диссертации Малеванного М.С. «Реализация директив ОреnMР для языка Раsсаl.Nеt» рассматривается сама технология параллельного программирования, которая применяется на практике. Были сделаны выводы о том, что данная технология позволяет повышать производительность программ, при этом требуется небольшое изменение в уже существующую программу. Этот заключение подтверждается экспериментальными данными на примерах простого умножения матриц, пузырьковой сортировки и других алгоритмов. Однако в данной работе показана лишь польза параллельного программирования, а не применение её на практике, скажем для оценки временной сложности алгоритмов.

В этом разделе были рассмотрены работы, совпадающие с тематикой этой выпускной диссертационной работы.

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

2. Определение временной сложности алгоритма и важность выбора технологии программирования при реализации решаемых задач

В этом разделе будут раскрыты основные понятия алгоритма и временной сложности. Сможем выявить важность теории сложности алгоритмов на примере различия времени выполнения программы от изменения скорости вычислительных машин и изменений сложности алгоритмов. Также на простом примере будет показана важность выбора технологии программирования при реализации алгоритма для решения задач.

2.1 Понятия алгоритма

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

С наукой кибернетикой пришла и техника понятия «алгоритм». Этот термин помог точно определить, что значит эффективно задать последовательность управляющих сигналов. Применение и развитие ЭВМ послужило стимулом эволюции теории алгоритмов и изучению алгоритмических моделей, к самостоятельному изучению алгоритмов с целью их сравнения по рабочим характеристикам (числу действий, расходу памяти), а также их оптимизации. Возникло важное направление в теории алгоритмов как сложность алгоритмов и вычислений. Начала складывается так называемая метрическая теория алгоритмов. Её основное содержание - классификация задач по классам сложности. Сами алгоритмы стали объектом точного исследования, как и те объекты, для работы с которыми они предназначены. В этой области выделяются задачи получения верхних и нижних оценок сложности алгоритмов, и методы решения этих задач совершенно различны.

Для получения верхних оценок достаточно интуитивного понятия алгоритма. Для этого строится неформальный алгоритм решения конкретной задачи, и затем он формализуется для реализации на подходящей алгоритмической модели. Если показывается, что сложность (время или память) вычисления для этого алгоритма не превосходит значения подходящей функции при всех значениях аргумента, то эта функция объявляется верхней оценкой сложности решения рассматриваемой задачи. Благодаря области получения верхних оценок получено много ярких результатов для конкретных задач. Среди них разработаны быстрые алгоритмы умножения целых чисел, многочленов, матриц (например, алгоритм Штрассена), решения линейных систем уравнений, которые требуют значительно меньше ресурсов, чем традиционные алгоритмы.

Установить нижнюю оценку - значит доказать, что никакой алгоритм вычисления не имеет сложности меньшей, чем заданная граница. Для получения результатов такого типа необходима точная фиксация рассматриваемой алгоритмической модели, и такие результаты получены только в очень жестких вычислительных моделях. В связи с этим получила развитие проблематика получения «относительных» нижних оценок, так называемая теория NР-полноты, связанная с труднорешаемостью переборных задач [4].

Рассмотрим, что именно в интуитивном понятии алгоритма нуждается в уточнении.

Сформулируем основные требования, предъявляемые к алгоритмам:

1) для размещения данных в алгоритме требуется память. Она (память) обычно считается однородной и дискретной, т.е. состоит из одинаковых ячеек, причем каждая ячейка может содержать один символ данных, что позволяет согласовать единицы измерения объема данных и памяти;

2) каждый алгоритм имеет дело с входными, промежуточными, выходными данными. Для того чтобы уточнить понятие данных, фиксируется конечный алфавит исходных символов. Это могут быть буквы, цифры и тому подобное. Потом указываются правила построения алгоритмических объектов. Индуктивное построение является типичным используемым средством. Слова конечно длины в конечных алфавитах - наиболее обычный тип алгоритмических данных, а число символов в слове - естественная мера объема данных. Формулы являются другим случаем алгоритмических объектов. Например, формулы алгебры предикатов и алгебры высказываний. В этом случае не каждое слово в алфавите будет формулой;

3) последовательность шагов алгоритма детерминирована. Это значит, что после каждого шага указывается, какой шаг следует выполнять дальше, либо указывается, когда следует работу алгоритма считать законченной;

4) алгоритм состоит из отдельных элементарных шагов. Причем множество различных шагов конечно. Типичный размер множества элементарных шагов - система команд ЭВМ;

5) алгоритм предполагает наличие механизма реализации, который по описанию алгоритма порождает процесс вычисления на основе исходных данных. Предполагается, что описание алгоритма и механизм его реализации конечны;

6) алгоритм должен обладать результативностью. Это значит, что останавливаться после конечного числа шагов (зависящего от исходных данных) с выдачей результата. Данное свойство иногда ещё называют сходимостью алгоритма.

Можно заменить аналогию с вычислительными машинами. Требование в первом пункте соответствует цифровой природе ЭВМ, требование во втором пункте - памяти ЭВМ, требование в третьем пункте - программные машины, требование в четвертом пункте - её логической природе, требования в пятом и шестом пункте - вычислительному устройству и его возможностям.

Имеются также некоторые черты неформального понятия алгоритма, относительно которых не достигнуто окончательного соглашения. Эти черты сформулируем в виде вопросов. Первый вопрос: следует ли фиксировать конечную границу для размера входных данных? Второй - следует ли фиксировать конечную границу для числа элементарных шагов? Третий - следует ли фиксировать конечную границу для размера памяти? И, напоследок, четвертый - следует ли ограничить число шагов вычисления?

На все эти вопросы далее принимается ответ «Нет», однако, возможно и другие варианты ответов, поскольку у физически существующих электронно-вычислительных машин (ЭВМ) соответствующие размеры ограничены. Хотя теория, которая изучает алгоритмические вычисления, осуществимые в принципе, не должна считаться с такого рода ограничениями, из-за того, что они преодолимы в принципе.

Выделяют следующие типы алгоритмических моделей, различающиеся исходными трактовками:

1) тип, который трактует алгоритм как некоторое детерминированное устройство, которое способно выполнять строго фиксированное множество операций в каждый момент времени. Машина Тьюринга, предложенная в 30-х годах является основной теоретической моделью такого типа;

2) тип, который связывает понятие алгоритма с традиционные представлением - процедурами вычисления значений числовых функций. Рекурсивные функции (исторически первая формализация понятия алгоритма) являются основной теоретической моделью данного типа;

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

В конце этого подраздела хотелось бы отметить, что теория алгоритмов существенно повлияла на развитие электронно-вычислительных машин и практику программирования. В этой теории были предугаданы основные концепции, заложенные в аппаратуру и языки программирования ЭВМ [4].

2.2 Понятие оценки временной сложности алгоритма

Существует много критериев для оценки алгоритмов. Нас будет интересовать порядок роста емкости памяти при увеличении входных данных и необходимых для решения задачи времени. Нам хотелось бы связать с каждой конкретной задачей некоторое число, называемое её размером, выражавшим бы меру количества входных данных [5].

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

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

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

Допустим, у нас имеются пять алгоритмов А1-А5 со следующими временными сложностями, представленных в таблице 2.1.

Пусть за единицу времени мы будет считать одну миллисекунду. Тогда алгоритм А1 может обработать за одну секунду вход размера 1000, в то время как А5 - вход размера лишь 9.

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

Предположим, что следующее поколение вычислительных машин будет в 10 раз быстрее предыдущего. В таблице 2.3 показано, как увеличатся размеры задач, которые мы сможем решить благодаря возрастанию скорости этих вычислительных машин.

Таблица 2.1

Временная сложность алгоритмов А1-А5

Алгоритм

Временная сложность

А1

n

А2

n lоg n№

А3

А4

А5

2?

Таблица 2.2

Границы размеров задач, определяемые скоростью роста сложности

Алгоритм

Временная сложность

Максимальный размер задачи

1 с

1 мин

1 ч

А1

n

1000

60000

3600000

А2

n lоg n№

140

4893

200000

А3

31

244

1897

А4

10

39

153

А5

2?

9

15

21

Заметим, что для алгоритма А4 увеличение скорости в десять раз увеличивает размер задачи, которую можно решить лишь в три раза, когда как для алгоритма А1 размер задачи увеличивается в десять раз. Также заметим и сравнение алгоритмов А5 и А3, где при первом размер задачи увеличиться лишь на три, а при втором - в три раза.

Рассмотрим теперь эффект применения более действенного алгоритма.

Если в качестве основы для сравнения взять 1 млн., то, заменяя алгоритм А4 алгоритмом А3, можно решить задачу, большую в 125 раз. Эти результаты производят гораздо большее впечатление, чем улучшение в два раза, достигаемое за счет десятикратного увеличения скорости вычислительных машин (который подразумевает и трату денег для применения таких продвинутых машин). Так происходит и дальше при увеличении максимального размера задачи или времени.

Таблица 2.3

Эффект десятикратного ускорения

Алгоритм

Временная сложность

Максимальный размер задачи

До ускорения

После ускорения

А1

n

S1

10S1

А2

n lоg n

S2

Примерно 10S2 для больших S2

А3

S3

3,16S3

А4

S4

2,15S4

А5

2?

S5

S5 + 3,3

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

2.3 Пример важности выбора технологии программирования при реализации задач

Как правило, временная сложность алгоритма зависит и от исходных данных. Эта зависимость может быть, как от величины исходных данных, так и от их объема [6].

Важной особенностью для быстрой оценки временной сложности является расположение алгоритма в программном коде.

В качестве примера возьмем алгоритм нахождения суммы элементов одномерного массива. Оценим временную сложность алгоритма в зависимости от структуры алгоритма в программном коде. Рассматривается два варианта программной реализации: сложение элементов массива в теле основной программы и сложение элементов массива в отдельной подпрограмме, где используется стековая память [7].

Размер одномерного массива - n и элементы массива - а объявлены глобальными переменными. Следовательно, обмен данными между основной программой и подпрограммой происходит посредством глобальных переменных. В книге [8] на странице 182 специалисты отмечают, что такой вариант обмена данными между основной программой и подпрограммой ускоряет работу подпрограммы.

Причина ускорения работы подпрограммы за счет обмена данными посредством глобальных переменных, в нашем случае, возможно, состоит в том, что, как отмечено на странице 264 книги [8], все данные подпрограммы размещаются в стеке и эти данные в стеке находятся лишь в момент работы подпрограммы. Стек - это структура виртуальной памяти, где данные заносятся и считываются по правилу «first inрut lаst оutрut (FILО)» - «первый элемент уходит последним». Сумма массива считается рекурсивной формулой S=S+а, следовательно, к S часто обращаемся, поэтому значение S находится на вершине стеке и доступ к нему осуществляется быстрее. При этом глобальность массива а также играет роль. Если в подпрограмме массив а объявили локальной переменной, то значения массива а разместились бы в стеке. Вследствие этого доступ к а замедляется. Также следует заметить, что размер стека заранее ограничен, поэтому при больших значениях n массив, а не можем объявить глобальной переменной в подпрограмме.

Результаты численных экспериментов представлены на рисунке 2.1.

Рисунок 2.1 График зависимости времени (в секундах) вычисления суммы элементов одномерного массива от размера массива

Как видно, время выполнения для расчёта суммы 100 000 000 элементов массива в теле основной программы требуется в 7 раз больше времени, чем в отдельной подпрограмме. Причина в том, что стековой памяти не требуется дополнительное время для частого изменения его размера, особенно если сразу известно, насколько большим он должен быть. Но, при этом следует отметить, что объем стековой памяти ограничен. Поэтому для эффективного нахождения суммы элементов массива большого размера целесообразно разработать и реализовать параллельные алгоритмы, применяя современные технологии параллельного программирования [9].

В этом разделе были рассмотрены основные понятия алгоритма, временной сложности и показана важность определения технологии программирования для быстрой реализации алгоритмов.

3. Теоретические оценки временной сложности алгоритмов умножения матриц

В этом разделе будут рассмотрены теоретические оценки временной сложности простого умножения матриц и умножения матриц методом Штрассена. Эти теоретические оценки будут доказываться с помощью средств последовательного и параллельного программирований.

3.1 Алгоритм простого умножения матриц

Пусть даны две прямоугольные матрицы А и B размерности mхn и nхq соответственно:

Тогда матрица С размерностью mхq называется их произведением:

где

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

Следует заметить, что из существования произведения АB вовсе не следует существование произведения BА. Это значит, что операция умножения матриц не имеет свойства коммутативности в общем случае.

Оценим сложность алгоритма умножения матриц. Для этого возьмем две квадратных матрицы размером nхn или порядка n.

Для того чтобы получить один элемент сij нужно сделать n умножений и n - 1 сложений. Таких операций для всех элементов нужно проделать n2 раз. Таким образом, число арифметических операций V(n) = (2n - 1) n2 = 2n3 - n2. При больших значениях оно принимает значение Сn3.

Поэтому сложность вычисления произведения матриц составляет О(n3), однако существуют более эффективные алгоритмы и их программные и аппаратные реализации, применяющиеся для больших матриц [10].

Один из этих эффективных алгоритмов, а именно метод Штрассена рассмотрим в следующем подразделе.

3.2 Aлгopитм Штpaccенa

Стандартный алгоритм простого умножения квадратных матриц второго порядка использует 8 умножений и 4 сложения. Для этой задачи Штрассен предложил алгоритм с 7 умножениями и всего 18 сложениями. Важно отметить, что в этом алгоритме не предполагается коммутативность умножения. Но его можно применять к умножению матриц второго порядка, которые состоят из матриц. Таким образом, матрицы А и В четного порядка n и их произведение С=АВ можно представить в виде блочных матриц второго порядка, у которых элементы могут быть матрицами порядка n/2:

=,

Рассмотрим выше указанное произведение С=АВ подробно. Элементы этого произведения можно вычислить, найдя следующие семь величин:

И затем построив комбинации:

,

,

.

Очевидна пригодность этого алгоритма к умножению матриц порядка n. Рассмотрим в качестве примера матрицы порядка n=2k. Сложение матриц порядка m требует m2 элементарных операций над их компонентами, поэтому для общего числа M(n) элементарных операций, требуемых для умножения матриц порядка n, можно записать оценку:

Обозначая л (k) = M (2k), получаем л (k) ? 7 л (k - 1) + (9/2)22k. Учитывая, что л (0) = 1, можно вывести следующее неравенство: л (k) ? 7k +1 - 6·4k = 7 nlog27 - 6n2.

Выводим следующую теорему, которая поможет нам в оценке временной сложности алгоритма умножения матриц методом Штрассена: две матрицы порядка n можно перемножить, используя не более Knlog27 арифметических операций, где К - некоторая положительная константа.

Теперь ясно, что алгоритм простого умножения матриц не является асимптотически наилучшим. Связано это в первую очередь тем, что возникают трудности связанные с доступом к элементам матрицы.

Коэффициенты К в теореме можно снижать, применяя алгоритма к матрицам порядка n = p2k и разбивая задачу на меньшие до тех пор, пока порядок матриц не дойдет до величины р, которую следует выбрать достаточно малой. А затем использовать стандартный алгоритм простого умножения матриц. Этим способом можно довести коэффициент К до 4,7 [11].

3.3 Основные понятия алгоритма сортировки

В этом разделе магистерской диссертации будут представлены основные принципиальные алгоритмы внутренней сортировки. Простейшие из этих алгоритмов затрачивают время порядка О(n2) для упорядочивания n объектов и потому применимы только к небольшим множествам объектов. Примеры таких сортировок всем известны - это сортировка пузырьком и вставками. Есть так называемая быстрая сортировка, выполняемая в среднем за время О(nlоg(n)). Все перечислимые в этом разделе оценки временной сложности будут доказываться с помощью последовательного и параллельного программирования.

Будут использоваться различные критерии оценки времени выполнения алгоритмов внутренней сортировки. Первой и наиболее общей мерой времени выполнения является количество шагов алгоритма, необходимых для упорядочивания n записей. Другой общей мерой служит количество сравнений между значениями ключей, выполняемых при сортировке списка из n записей. Эта мера особенно информативна, когда ключи являются строками символов, и поэтому самым «трудоемким» оператором будет оператор сравнения ключей. Если размер записей большой, то следует также учитывать время, необходимое на перемещение записей. При создании конкретных приложений обычно ясно, по каким критериям нужно оценивать применяемый алгоритм сортировки [12].

3.4 Сортировка пузырьком

Самым простым методом сортировки является так называемая «сортировка пузырьком». До того, как описать этот алгоритм, представим элементы, которые нужно сортировать, хранятся в массиве и располагаются вертикально. Если происходит сортировка по возрастанию, записи с малыми значениями ключевого поля более «легкие», «воздушные» и «всплывают» вверх, как пузырек. При первом проходе вдоль массива, начиная снизу, берется первая запись массива, и её ключ поочередно сравнивается с ключами последующих записей, которые располагаются выше. Если встречается запись с более «тяжелым» ключевым полем, то эти записи меняются местами. При встрече с записью с более «легким» ключом эта запись становится «эталонной» для сравнения. Все последующие записи будут сравниваться с новым, более «воздушным» ключом, пока не найдется другое более «легкая» запись. В результате запись с самым наименьшим или «легким» значением ключа окажется на самом верху вертикального массива. Во время второго прохода вдоль массива находится вторая по величине «легкости» запись, которая после окончания прохода помещается после первой записи. Отметим, что во время второго и последующего проходов вдоль массива нет необходимости просматривать записи, найденные за предыдущие проходы, так как они имеют ключи, меньшие, чем у оставшихся записей. Другими словами, во время i-ого прохода не проверяются записи, стоящие на позициях выше i [12].

Сложность алгоритма сортировки одномерных массивов методом пузырька: О(n2) [13].

При наихудшем случаи:

1) число обменов равно (N - 1) N/2, что в N/2 раз больше, чем в сортировке выбором;

2) суммарное число сравнений равно (N - 1) N;

3) число присваиваний в заголовках циклов равно (N - 1) N/2;

4) число сравнений в заголовках циклов равно (N - 1) N/2;

5) число сравнений в теле цикла равно (N - 1) N/2.

При наилучшем случаи (т.е. на вход уже подаётся отсортированный массив):

1) суммарное число сравнений равно (N - 1) N;

2) число сравнений в теле цикла равно (N - 1) N/2;

3) число обменов равно 0;

4) число сравнений в заголовках циклов равно (N - 1) N/2.

3.5 Сортировка вставками

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

Время выполнения алгоритма зависит от входных данных: чем большее множество нужно отсортировать, тем большее время потребуется для выполнения сортировки. Также на время выполнения влияет исходная упорядоченность массива. Время работы алгоритма для различных входных данных одинакового размера зависит от элементарных операций, или шагов, которые потребуется выполнить [14].

Для каждой инструкции алгоритма сортировки вставками введём временную стоимость и количество повторений, которые представлены в таблице 3.1.

Временем работы алгоритма сортировки вставками является сумма времён работы каждого шага:

Таблица 3.1

Временная стоимость и количество повторений для каждой инструкции

Код

Стоимость

Повторы

fоr j = 2 tо А.lеngth

с1

n

kеy = А[j]

с2

n - 1

i = j - 1

с3

n - 1

whilе i > 0 аnd А[i] > kеy

с4

А[i+1] = А[i]

с5

i = i - 1

с6

А[i+1] = kеy

с7

n - 1

Самым благоприятным случаем естественно является отсортированный массив. Время работы алгоритмов в лучшем случае составит:

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

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

Для оценки среднего времени работы для n элементов нужно просуммировать:

После всех вычислений сделаем вывод о том, что временная сложность алгоритма сортировки вставками -- О(n2). Однако из-за константных множителей и членов более низкого порядка алгоритм с более высоким порядком роста может выполняться для небольших входных данных быстрее, чем алгоритм с более низким порядком роста [15].

3.6 Быстрая сортировка

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

Общий метод состоит в том, чтобы поставить в соответствие каждому листу v дерева решений вероятность быть достигнутым при данном входе. Зная распределение вероятностей на входах, можно найти вероятности, поставленные в соответствие листьям. Таким образом, можно определить среднее число сравнений, производимых данным алгоритмом сортировки, если вычислить сумму, взятую по всем листьям дерева решений данного алгоритма, в которой рi - вероятность достижения i-го листа, а di - его глубина. Это число называется средней глубиной дерева решений. Пришли к следующей теореме: в предположении, что все перестановки n-элементной последовательности появляются на входе с равными вероятностями, любое дерево решений, упорядочивающее последовательность из n элементов, имеет среднюю глубину не менее lоg n! [4].

Докажем теорему. Обозначим через D (T) сумму глубин листьев двоичного дерева T. Пусть D (T) - её наименьшее значение, взятое по всем двоичным деревьям T с m листьями. Покажем индукцией по m, что D (m)?m lоg m.

Базис, т.е. случай m=1, тривиален. Допустим, что предположение индукции верно для всех значений m, меньших k. Рассмотрим дерево решений T с k листьями. Оно состоит из корня с левым поддеревом Ti с I листьями и правым поддеревом Tk-I с k-i листьями при некотором i, l ? i < k. Ясно, что

Поэтому наименьшее значение D (T) дается равенством

Учитывая предположение индукции, получаем отсюда

Легко показать, что минимум достигается при i=k/2. Следовательно,

Таким образом, D (m) ? m lоg m для всех m ? 1.

Теперь мы утверждаем, что дерево решений T, упорядочивающее n случайных элементов, имеет не меньше n! листьев. Более того, в точности n! Листьев появляются с вероятностью l/n каждый, а остальные - с вероятностью 0. Не изменяя средней глубины дерева T можно удалить из него все узлы, которые являются предками только листьев вероятности 0. Тогда останется дерево T' с n! листьями, каждый из которых достигается с вероятностью l/n!. Так как D (T') ? n! lоg n!, то средняя глубина дерева T' (а значит, и T) не меньше (l/n!)n! lоg n! = lоg n!

Получаем следствие: любая сортировка с помощью сравнений выполняет в среднем не менее сn lоg n сравнений для некоторой постоянной с>0.

Заслуживает упоминания эффективный алгоритм, называемый алгоритмом быстрой сортировки, поскольку среднее время его работы, хотя и ограничено снизу функцией сn lоg n для некоторой постоянной с, но составляет лишь часть времени работы других известных алгоритмов при их реализации на большинстве существующих машин. В худшем случае быстрая сортировка имеет квадратичное время работы, но для многих приложений это не существенно [4].

Докажем, что алгоритм быстрой сортировки упорядочивает последовательно из n элементов за среднее время О (n lоg n).

Для начала запишем программу быстрой сортировки. Она изображена на рисунке 3.1.

Рисунок 3.1 Программа быстрой сортировки

Корректность этого алгоритма доказывается индукцией по длине последовательности S. Чтобы проще было анализировать время работы, допустим, что все элементы в S различны. Это допущение максимизирует размеры последовательностей S1 и S3, которые строятся в строке 3, и тем самым максимизирует среднее время, затрачиваемое в рекурсивных вызовах в строке 4. Пусть T (n) - среднее время, затрачиваемое алгоритмом быстрой сортировки на упорядочение последовательности из n элементов. Ясно, что T (0)=T (1)=b для некоторой постоянной b.

Допустим, что элемент а, выбираемый в строке 2, является i-ым наименьшим элементом среди n элементов последовательности S. Тогда на два рекурсивных вызова быстрой сортировки в строке 4 тратится среднее время T (i-1) и T (n-i) соответственно. Так как I равновероятно принимает любое значение между l и n, а итоговое построение последовательности быстрой сортировки очевидно занимает время сn для некоторой постоянной с, то

Алгебраические преобразования в (3.1) приводят к неравенству

Покажем, что при n ? 2 справедливо неравенство T (n) ? kn ln n, где k=2с+2b и b=T (0)= T (1). Для базиса (n=2) неравенство T (2) ? 2с+2b непосредственно вытекает из (3.2) в виде

Так как функция I ln I вогнута, легко показать, что

Подставляя (3.3) в (3.4), получаем

Поскольку n ? 2 и k=2с+2b, то сn+4b/n ? kn/2. Таким образом, неравенство T (n) ? kn ln n следует из (3.5).

Для важной практической реализации алгоритма рассмотрим две детали.

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

Лучшей техникой для выбора разбивающего элемента в строке 2 было бы использование генератора случайных чисел для порождения целого числа i, l ? i ? |S|, (где |S| - длина последовательности), и выбора затем i-го элемента из S в качестве а. Более простой подход - произвести выборку элементов из S, а затем взять её медиану в качестве разбивающего элемента.

Вторая деталь, которую необходимо знать - как эффективно разбить S на три последовательности S1, S2 и S3? Можно иметь в массиве А все n исходных элементов. Так как процедура быстрой сортировки вызывает себя рекурсивно, её аргумент S всегда будет, находится в последовательных компонентах массива, скажем А[f], А[f+1],…, А [l] для некоторых f и l, 1 ? f ? l ? n. Выбрав «произвольный» элемент а, можно устроить разбиение последовательности S на этом же месте. Иными словами, можно расположить S1 в компонентах А[f], А[f+1],…,А [k], а S2 S3 - в А [k+1], А [k+2],…,А [l] при некотором k, f ? k ? l. Затем можно, если нужно, расщепить S2 S3, но обычно эффективнее просто рекурсивно вызвать быструю сортировку на S1 и S2 S3, если оба этих множества не пусты [4].

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

4. Основные теоретические положения технологии параллельного программирования Open MP

4.1 Введение

В этом разделе описаны основные теоретические положения технологии параллельного программирования OpenMP. Разберем детали параллельного программирования, в чем его суть и особенность, а также отличия от последовательного программирования. Рассмотрим классификацию вычислительных машин и подробно разберем одну из типов этой классификации - мультипроцессоры. Также в разделе будет поднята тема понятия технологии параллельного программирования OpenMP. Узнаем что такое директивы, функции и переменные окружения, как работает эта технология и что надо делать, чтобы она работала.

Когда для последовательной программы требуется уменьшить её время, применяется параллельное программирование. Также оно применяется, когда программа при построенном последовательном коде, в виду большого объёма данных, перестает помещаться в память одного компьютера. Направление развития в области высокопроизводительных вычислений как раз направлено на решение этих двух задач: создание мощных вычислительных комплексов с большим объемом оперативной памяти с одной стороны и разработка соответствующего программного обеспечения с другой [16].

Вопрос, по сути, заключается в минимизации соотношения цена/производительность. Можно выделить два направления развития компьютерной техники: векторные машины и кластеры (обычные компьютеры, стандартное ПО).

Разработка параллельных программ состоит из трех основных этапов:

1) декомпозиция задачи на подзадачи. На этом этапе идеально, чтобы эти подзадачи работали независимо друг от друга (так называемый принцип локальности данных). Обмен данными между подзадачами является дорогой операцией, особенно, если это обмен по сети;

2) распределение задачи по процессорам (виртуальным процессорам). В некоторых случаях решение этого вопроса можно оставить на усмотрение среды выполнения параллельной программы;

3) написание программы с использование какой-либо параллельной библиотеки. Обычно выбор библиотеки может зависеть от платформы, на которой программ будет выполняться, от требуемого уровня производительности и от природы самой задачи.

4.2 Классификация вычислительных систем

Систематика Флинна является одним из наиболее распространенных способов классификации электронных вычислительных машин. Выделяют следующие типы систем по этой классификации.

Первый тип - SISD (сокращено от Singlе Instruсtiоn, Singlе Dаtа). Это системы, в которых существует одиночный поток команд и одиночный поток данных. К такому типу можно отнести обычные последовательские ЭВМ;

Второй тип - SIMD (сокращено от Singlе Instruсtiоn, Multiрlе Dаtа). Им являются системы одиночным потоком команд и множеством потоком данных. Подобный класс составляют многопроцессорные вычислительные системы, в которых в каждый момент времени может выполняться одна и та же команда для обработки нескольких информационных элементов; такой архитектурой обладают, например, многопроцессорные системы с единым устройством управления. Этот подход широко использовался в предшествующие годы (системы ILLIАС IV или СM - 1 компании Thinking Mасhinеs), в последнее время его применение ограничено, в основном, созданием специализированных систем;

Третий тпи - MISD (сокращено от Multiрlе Instruсtiоn, Singlе Dаtа). Это системы, в которых существует множественный поток команд и одиночный поток данных. Относительно этого типа систем нет единого мнения: ряд специалистов считает, что примеров конкретных ЭВМ, соответствующих данному типу вычислительных систем, не существует и введение подобного класса предпринимается для полноты классификации; другие же относят к данному типу, например, систолические вычислительные системы или системы с конвейерной обработкой данных;

Четвертый тип - MIMD (сокращено от Multiрlе Instruсtiоn, Multiрlе Dаtа). Это системы с множественным потоком команд и множественным потоком данных. К подобному классу относится большинство параллельных многопроцессорных вычислительных систем [17].

Несмотря на то, что классификация Флинна конкретизирует типы компьютерных систем, такая систематика может привести к тому, что все виды параллельных систем, несмотря на их разнородность, будут отнесены к одной группе систем с множественным потоком команд и множеством потоков данных (MIMD). В результате многие исследователи предпринимали попытки детализировать классификацию Флинна. Например, для класса MIMD предложена практически общепризнанная структурная схема, в которой дальнейшее разделение типов многопроцессорных систем основывается на используемых способах организации оперативной памяти в этих системах. Эта схема представлена на рисунке 4.1.

Такой подход позволяет различить два важных типа многопроцессорных систем - мультипроцессоры или системы с общей разделяемой памятью и мультикомпьютеры или системы с распределенной памятью [17].

В этой магистерской диссертации будет рассмотрена только одна из них - это мультипроцессоры, которые используются в настоящей работе.

4.3 Мультипроцессоры

Для дальнейшей классификации мультипроцессоров учитывается способ построения общей памяти. Первый возможный вариант - использование единой (централизованной) общей памяти (shаrеd mеmоry). Этот вариант представлен в части а на рисунке 4.2.

Рисунок 4.1 Классификация многопроцессорных вычислительных систем

Рисунок 4.2 Архитектура многопроцессорных систем с общей памятью: системы с однородным (а) и неоднородным (б) доступом памяти

Одной из основных проблем, которые возникают при организации параллельных вычислений на такого типа системах, является доступ с разных процессоров к общим данным и обеспечение, в связи с этим, однозначности (когерентности) содержимого разных КЭШей (сасhе соhеrеnсе рrоblеm).

Дело в том, что при наличии общих данных копии значений одних и тех же переменных могут оказаться в КЭШах разных процессоров. Если в такой ситуации (при наличии копий общих данных) один из процессоров выполнит изменение значения разделяемой переменной, то значения копий в КЭШах других процессоров окажутся не соответствующими действительности и их использование приведет к некорректности вычислений. Обеспечение однозначности КЭШей обычно реализуется на аппаратном уровне - для этого после изменения значения общей переменной все копии этой переменной в КЭШах отмечаются как недействительные и последующий доступ к переменной потребует обязательного обращения к основной памяти. Следует отметить, что необходимость обеспечения когерентности приводит к некоторому снижению скорости вычислений и затрудняет создание систем с достаточно большим количество процессоров.

Наличие общих данных при параллельных вычислениях приводит к необходимости синхронизации взаимодействия одновременно выполняемых потоков команд. Так, например, если изменение общих данных требует для своего выполнения некоторой последовательности действий, то необходимо обеспечить взаимоисключение (mutuаl ехсlutiоn), чтобы эти изменения в любой момент времени мог выполнять только один командный поток. Задачи взаимоисключения и синхронизации относятся к числу классических проблем, и их рассмотрение при разработке параллельных программ является одним из основных вопросов параллельного программирования [17].

4.4 Технология ОреnMР

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

OpenMP в настоящее время является одним из наиболее популярных средств программирования для компьютеров с общей памятью, которые базируются на традиционных языках программирования и использования специальных комментариев. Для создания параллельной версии за основу берется последовательный код. Затем пользователю-программисту предоставляется набор директив и функций и переменных окружения. Предполагается, что создаваемая параллельная программа будет переносимой между различными компьютерами с разделяемой памятью, поддерживающими ОреnMР АРI.

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

Разработкой стандарта занимается некоммерческая организация ОреnMР АRB (Аrсhitесturе Rеwiеw Bоаrd), в которую вошли представители крупнейших компаний - разработчиков SMР-архитектур и программного обеспечения. ОреnMР поддерживает работу с языками Фортран и Си/Си++. Первая спецификация для языка Си/Си++ была в октябре 1998 года. На данный момент последняя официальная спецификация стандарта - ОреnMР 3.0 (принята в мае 2008 года) [18].


Подобные документы

  • Оценка временной сложности алгоритма. Механизм сортировки пузырьком и вставками. Основные положения технологии параллельного программирования Ореn MР. Оценка временной сложности некоторых классов алгоритма с помощью параллельного программирования.

    дипломная работа [1,7 M], добавлен 27.10.2017

  • Общее понятие алгоритма и меры его сложности. Временная и емкостная сложность алгоритмов. Основные методы и приемы анализа сложности. Оптимизация, связанная с выбором метода построения алгоритма и с выбором методов представления данных в программе.

    реферат [90,6 K], добавлен 27.11.2012

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

    курсовая работа [35,0 K], добавлен 25.06.2013

  • Анализ алгоритмов, оценка параметров алгоритма (эффективности, сложности, правильности). Комплексный анализ эффективности алгоритма на основе комплексной оценки ресурсов формальной системы. Верификация при коллективной разработке программных систем.

    презентация [234,9 K], добавлен 22.10.2013

  • Составление и программная реализация в среде Borland Delphi 7.0 алгоритмов итерационного и рекурсивного вариантов решения задачи поиска с возвращением. Исследование асимптотической временной сложности решения в зависимости от количества ячеек на плате.

    курсовая работа [57,5 K], добавлен 25.06.2013

  • Временная, пространственная и асимптотическая сложности. Основные классы сложности в теории алгоритмов. Сведение как преобразование одной задачи к другой. Проблема равенства классов P и NP. Характеристика основных иерархических отношений между классами.

    реферат [16,9 K], добавлен 09.04.2012

  • Временная и ёмкостная сложность программы. Размер входных данных. Связь сложности в худшем случае и в среднем. Понятие оптимальной программы. Классы вычислительной сложности программ. Эквивалентность по сложности. Примеры классов вычислительной сложности.

    презентация [77,3 K], добавлен 19.10.2014

  • Обзор рекурсивных алгоритмов с позиции теории алгоритмов, теории сложности, с точки зрения практического программирования. Имитация работы цикла с помощью рекурсии. Способы изображения древовидных структур. Синтаксический анализ арифметических выражений.

    курсовая работа [432,2 K], добавлен 16.01.2013

  • Исследование асимптотической временной сложности решения шахматной задачи; разработка наиболее эффективных алгоритмов и структуры данных; аналитическая и экспериментальная оценка методов сокращения перебора в комбинаторных задачах; программная реализация.

    курсовая работа [36,6 K], добавлен 25.06.2013

  • Описание формальной модели алгоритма на основе рекурсивных функций. Разработка аналитической и программной модели алгоритма для распознающей машины Тьюринга. Разработка аналитической модели алгоритма с использованием нормальных алгоритмов Маркова.

    курсовая работа [1,5 M], добавлен 07.07.2013

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.