Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Билеты по проге.docx
Скачиваний:
5
Добавлен:
01.07.2025
Размер:
630.61 Кб
Скачать

Метод ветвей и границ (доп. Из Wikipedia)

Общая идея метода может быть описана на примере поиска минимума и максимума функции f(x) на множестве допустимых значений x. Функция f и x могут быть произвольной природы. Для метода ветвей и границ необходимы две процедуры: ветвление и нахождение оценок (границ).

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

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

В основе метода ветвей и границ лежит следующая идея (для задачи минимизации): если нижняя граница для подобласти A дерева поиска больше, чем верхняя граница какой-либо ранее просмотренной подобласти B, то A может быть исключена из дальнейшего рассмотрения (правило отсева). Обычно, минимальную из полученных верхних оценок записывают в глобальную переменную m; любой узел дерева поиска, нижняя граница которого больше значения m, может быть исключен из дальнейшего рассмотрения.

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

Задача коммивояжёра: При этом на его маршрут накладывается два ограничения:

· маршрут должен быть замкнутым, то есть коммивояжер должен вернуться в

тот город, из которого он начал движение;

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

есть надо обязательно обойти все города, при этом не побывав ни в одном

городе дважды.

(n-1)!/2 маршрутов.

Билет 14. Простые методы сортировки.

Сортировка – процесс перегруппировки заданного множества объектов в некотором определённом порядке. Метод сортировки называется устойчивым, если в процессе сортировки относительное расположение элементов с одинаковыми ключами не изменяется.

Для сортировки массивов методы должны экономно использовать память , сортировка на том же месте, нет передачи в результирующий массив b.

Хорошей мерой эффективности сортировки может быть С –число сравнений элементов массива; М – число перемещений элементов.

Хорошие алгоритмы требуют порядка n*logn сравнений, но в простых требуется порядка n^2.

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

Методы сортировки на том же месте делятся на 3 основные категории:

С помощью включения

С помощью выделения

С помощью обменов.

Рассмотрим 3 простых алгоритма сортировки:

  1. Сортировка методом вставки (прямого включения)

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

Такой случай повторяющегося процесса с 2 условиями окончания позволяет использовать метод барьера, поставить барьер а(0) = х. Тогда между j=I и x=a[i] вставить a[0]=x; и можно убрать проверку условия j>0. Расширить диапазон в A[n+1].

for (i=1;i<=N-1; i++)

{ j=i;

x=A [i];

while (( j>0)&&(x<A[ j-1 ])) //Пока не дошли до левого конца и пока элемент меньше предыдущего.

{ A [ j ]=A[ j-1 ]; //Ищем место, раздвигаем

j --;

}

A[ j ]=x;

}

Элементы до i-1 уже отсортированы, берем i-й элемент и вставляем его в нужное место (находим это место и сдвигаем пр. часть на 1).

Сортировка вставками — простой алгоритм сортировки. Хотя этот алгоритм сортировки уступает в эффективности более сложным (таким как быстрая сортировка), у него есть ряд преимуществ:

  1. прост в реализации;

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

  3. эффективен на наборах данных, которые уже частично отсортированы;

  4. это устойчивый алгоритм сортировки (не меняет порядок элементов, которые уже отсортированы);

  5. может сортировать список по мере его получения;

  6. не требует дополнительной памяти.

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

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

Так, лучшим случаем является отсортированный массив, а худшим - массив, отсортированный в порядке, обратном нужному.

Ci – число сравнений элементов при i-м просеивании (без метода барьера).

Ci min=1 Ci MAX= I(i) Ci average=(1+i)/2 CMIN=𝜮 (Ci min (от 1 до N-1) )= N-1

CMAX=𝜮 Ci MAX = N*(N-1)/2

CCP= =

Mi= Ci +1 Mmin=2(N-1) Mmax= Mcp= ≈(n^2)/4

Улучшение(модификация): Бинарные включения ( метод с двоичным включением):

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

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

О(N) в лучшем случае.

function insertionSort(a):

for i = 1 to n - 1

j = i - 1

k = BinSearch(a, a[i], 0, j)

for m = j downto k

swap(a[m], a[m+1])

  1. М етод поиска максимумов(с помощью прямого выбора)

for(i=0;i<=N-2;i++)

{ k= i ; x=A[i];

for(j=i+1;j<=N-1;j++)

if(A[j]<x)

{ k=j; x=A[k]; }

A[k]=A[i]; A[i]=x;

}

Число сравнений не зависит от начального порядка!

Ход алгоритма:

  1. находим минимальное значение в текущем списке ( выбирается элемент с наименьшим ключом)

  2. производим обмен этого значения со значением на первой неотсортированной позиции .

  3. теперь сортируем хвост списка, исключив из рассмотрения уже отсортированные элементы. ( Повторяется с оставшимися n-1, n-2 … элементами, пока не останется 1, самый большой элемент.

При прямом выборе для поиска одного элемента с наименьшим ключом просматриваются все элементы последовательности и найденный помещается как очередной в готовую последовательность.

Ci=(N-1)-(i-1)+1=N-i-1 Mi min=3 Mi max=Ci+3 Mi cp !!!

C= =(((N-1)+(N-N+2-1))/2)*(N-i) Mmin=3(N-1) Mmax= Mcp~n*ln n~O(n*log n)

М ср = n*(Ln(n)+g), g – константа Эйлера.

Алгоритм порядка н^2.

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

  • Если ключи вначале упорядочены или почти упорядочены, то прямое включение будет оставаться более быстрым.

  1. Метод пузырька

for(i=1;i<=N-1;i++)

{

for(j=N-1;j>=I;j--)

{

if(A[j-1]>A[j])

{ x=A[j-1]; A[j-1]=A[j]; A[j]=x; }

}

}

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

  • Внутренний цикл можно выполнять для j = 1,2,...,n − i, где i — номер итерации внешнего цикла (нумерация с единицы), так как на i-й итерации последние i элементов массива уже будут правильно упорядочены.

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

Ci min=Ci MAX=Ci cp=N-1-i+1=N-i C=

Mi min=0 Mi max=3*Ci Mi cp=(3/2)*Ci Mmin=0 Mmax=3C Mcp=(3/4)*(N^2-N)

Все вышеперечисленные методы сортировки имеют сложность O(n2).

Билет 15. Усовершенствованные методы сортировки.

Шейкерная сортировка:

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

Поэтому, чередуем направление последовательных просмотров.

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

#define SWAP(A,B) {(A)=(A)^(B); (B)=(A)^(B); (A)=(A)^(B);}

void m_sheker(int mas[], int n)

{

int last = n-1, left = 1, right = n-1, j;

do

{

for(j = right; j >= left; j--)

{

if(mas[j-1] > mas[j])

{

SWAP(mas[j-1], mas[j]);

last = j;

}

}

left = last + 1;

for(j = left; j <= right; j++)

{

if(mas[j-1] > mas[j])

{

SWAP(mas[j-1], mas[j]);

last = j;

}

}

right = last-1;

} while(left < right);

}

Наименьшее число сравнений в алгоритме Шейкер-сортировки  . Это соответствует единственному проходу по упорядоченному массиву (лучший случай).

Обратим внимание на то, что усовершенствования не влияют на число перемещений, они лишь сокращают число излишних двойных проверок. Так же имеет сложность О(n^2) в среднем и максимальном случаях, линейная в лучшем случае О(N). Он только чуть улучшает производительность и не улучшает асимптотику. Вставки будут оставаться наиболее предпочтительными.

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

Улучшение метода, основанного на обмене. Исходят из того, что сначала лучше переходить на большие расстояния. Выберем наугад элемент, просматриваем массив справа и слева, пока не нашли элемент больше и меньше х соответственно. Теперь меняем местами эти два элемента, пока оба просмотра не встретятся. В результате массив разбит на части с ключами <= х и >=х. Если в заголовках цикла while < и >. Так х выступает в роли барьера для обоих просмотров.

Выбираем в массиве некоторый элемент, который будем называть опорным элементом. С точки зрения корректности алгоритма выбор опорного элемента безразличен. С точки зрения повышения эффективности алгоритма выгоднее всего выбирать медиану; но без дополнительных сведений о сортируемых данных её обычно невозможно получить. Известные стратегии: выбирать постоянно один и тот же элемент, например, средний или последний по положению; выбирать элемент со случайно выбранным индексом. Часто хороший результат даёт выбор в качестве опорного элемента среднего арифметического между минимальным и максимальным элементами массива, особенно для целых чисел (в этом случае опорный элемент не обязан быть элементом сортируемого массива).

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

Два индекса — l и r, приравниваются к минимальному и максимальному индексу разделяемого массива, соответственно.

Вычисляется индекс опорного элемента m. [ m = a[(l+r)/2] ]

Индекс l последовательно увеличивается до тех пор, пока l-й элемент не окажется больше либо равен опорному.

Индекс r последовательно уменьшается до тех пор, пока r-й элемент не окажется меньше либо равен опорному.

Если r = l — найдена середина массива — операция разделения закончена, оба индекса указывают на опорный элемент.

Если l < r — найденную пару элементов нужно обменять местами и продолжить операцию разделения с тех значений l и r, которые были достигнуты. Следует учесть, что если какая-либо граница (l или r) дошла до опорного элемента, то при обмене значение m изменяется на r-й или l-й элемент соответственно, изменяется именно индекс опорного элемента и алгоритм продолжает свое выполнение.

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

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

Поскольку в каждой итерации (на каждом следующем уровне рекурсии) длина обрабатываемого отрезка массива уменьшается, по меньшей мере, на единицу, терминальная ветвь рекурсии будет достигнута обязательно, и обработка гарантированно завершится.

Анализ

Очень важно то, что при большой глубине рекурсии может возникнуть переполнение стека.

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

Сложность складывается из глубины рекурсии и количества сравнений.

Наилучший случай

В результате каждого разделения массив делится примерно пополам, потому максимальная глубина рекурсии, при которой длина = 1 будет Log(2)n. Итоговое кол-во сравнений даёт +n, что даёт сложность O(n*logn).

Средний случай

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

Будем считать «удачным» разделением такое, при котором опорный элемент окажется среди центральных 50 % элементов разделяемой части массива; ясно, вероятность удачи при случайном распределении элементов составляет 0,5. При удачном разделении размеры выделенных подмассивов составят не менее 25 % и не более 75 % от исходного. Поскольку каждый выделенный подмассив также будет иметь случайное распределение, все эти рассуждения применимы к любому этапу сортировки и любому исходному фрагменту массива.

Удачное разделение даёт глубину рекурсии не более  . Поскольку вероятность удачи равна 0,5, для получения   удачных разделений в среднем потребуется   рекурсивных вызовов, чтобы опорный элемент k раз оказался среди центральных 50 % массива. Применяя эти соображения, можно заключить, что в среднем глубина рекурсии не превысит  , что равно   А поскольку на каждом уровне рекурсии по-прежнему выполняется не более  операций, средняя сложность составит  .

Худший случай.

В самом несбалансированном варианте каждое разделение даёт два подмассива размерами 1 и  , то есть при каждом рекурсивном вызове больший массив будет на 1 короче, чем в предыдущий раз. Такое может произойти, если в качестве опорного на каждом этапе будет выбран элемент либо наименьший, либо наибольший из всех обрабатываемых. При простейшем выборе опорного элемента — первого или последнего в массиве, — такой эффект даст уже отсортированный (в прямом или обратном порядке) массив, для среднего или любого другого фиксированного элемента «массив худшего случая» также может быть специально подобран. В этом случае потребуется   операций разделения, а общее время работы составит   операций, то есть сортировка будет выполняться за квадратичное время. Но количество обменов и, соответственно, время работы — это не самый большой его недостаток. Хуже то, что в таком случае глубина рекурсии при выполнении алгоритма достигнет n, что будет означать n-кратное сохранение адреса возврата и локальных переменных процедуры разделения массивов. Для больших значений n худший случай может привести к исчерпанию памяти (переполнению стека) во время работы программы.

Достоинства:

  • Один из самых быстродействующих (на практике) из алгоритмов внутренней сортировки общего назначения.

  • Прост в реализации.

  • Требует лишь   дополнительной памяти для своей работы. (Не улучшенный рекурсивный алгоритм в худшем случае   памяти)

  • Допускает естественное распараллеливание (сортировка выделенных подмассивов в параллельно выполняющихся подпроцессах).

Недостатки:

  • Сильно деградирует по скорости (до  ) в худшем или близком к нему случае, что может случиться при неудачных входных данных.

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

  • Неустойчив.

Улучшения алгоритма направлены, в основном, на устранение или смягчение вышеупомянутых недостатков, вследствие чего все их можно разделить на две группы: придание алгоритму устойчивости и «защита от худшего случая» — устранение деградации производительности и переполнения стека вызовов из-за большой глубины рекурсии при неудачных входных данных.

Что касается первой проблемы, то она элементарно решается путём расширения ключа исходным индексом элемента в массиве. В случае равенства основных ключей сравнение производится по индексу, исключая, таким образом, возможность изменения взаимного положения равных элементов. Эта модификация не бесплатна — она требует дополнительно O(n) памяти и одного полного прохода по массиву для сохранения исходных индексов.

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

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

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

Случайный выбор. Вероятность случайного возникновения худшего случая становится исчезающе малой, а намеренный подбор — практически неосуществимым. Ожидаемое время выполнения алгоритма сортировки составляет O(n lg n).

Во избежание отказа программы из-за большой глубины рекурсии могут применяться следующие методы:

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

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

Сортировка Шелла

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

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

  • отсутствие потребности в памяти под стек;

  • отсутствие деградации при неудачных наборах данных — быстрая сортировка легко деградирует до O(n²), что хуже, чем худшее гарантированное время для сортировки Шелла.

  • первоначально используемая Шеллом последовательность длин промежутков:   в худшем случае, сложность алгоритма составит ;

Но известно, что расстояния не должны быть множителями друг друга.

void shell(int c[])

{

/* ... */

for (k = N/2; k>0; k/=2)

for (i = k; i < N; i++)

{

t = b[i];

for (j = i; j >= k; j -= k)

{

if (t < b[j - k])

b[j] = b[j - k];

else

break;

}

b[j] = t;

}

/* ... */

}

Билет 16. Алгоритмы генерации псевдослучайных чисел.

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

Раньше, числа получали без ЭВМ, а с их появлением – поиски эффективных методов. Интерес к получению случайных чисел с помощью арифметических операций. Одним из первых такой подход предложил Джон фон Нейман.