|
Лекции по структурам и алгоритмам обработки данных (СиАОД) 1210. Трудоемкость алгоритмов
В предыдущих разделах мы уже не раз сталкивались с оценкой трудоемкости алгоритмов, то есть объема необходимых расчетов. Часто эта важнейшая характеристика алгоритмов недооценивается студентами. Рассмотрим наглядный пример. Пусть требуется найти кратчайший путь на графе из 30 вершин, в котором каждая вершина связана со всеми другими. Как известно из начальной части курса [8], для решения этой задачи часто используют алгоритм Дейкстры, трудоемкость которого пропорциональна N2, где N – количество вершин графа. В математике для этого случая принято обозначение O(N2). Попробуем найти кратчайший путь перебором всевозможных путей, что проще всего реализуется поиском в глубину. Ограничимся для простоты только путями, включающими все вершины графа. Поскольку в промежутке между начальной и конечной вершинами могут в различном порядке находиться 28 вершин, то таких путей 28! Эта величина превышает 1029. Будем считать, что мы в состоянии находить и оценивать 109 путей в секунду (реально значительно меньше). Значит, нам потребуется 1020 секунд для просмотра всех путей. В сутках менее 105 секунд, а в году менее 500 суток, то есть поиск займет более 21012 лет. И это при том, что мы рассматривали только самые длинные по числу вершин пути! Нас не спасет даже компьютер, работающий в 1000 раз быстрее. Можно возразить, что кратчайший путь не должен включать так много промежуточных вершин. Но термин "длина” в задачах поиска путей на графе используется в обобщенном смысле. Например, речь может идти о стоимости самолетных сообщений. Легко вообразить, что перелеты на короткое расстояние совершают маленькие самолеты, билеты на которые обходятся дешевле. В первую очередь оценивают зависимость трудоемкости вычислений от размерности исходных данных N. Переборные алгоритмы имеют обычно экспоненциальную трудоемкость, то есть объем вычислений оценивается как O(Exp(N)). Такие алгоритмы реализуемы только при ограниченных значениях N. Полиномиальные алгоритмы имеют оценку O(NP), где P>0. Величина коэффициента пропорциональности отступает на второй план в тех случаях, когда размерность велика. Сравним, например, два алгоритма с трудоемкостью 0.1N2 и 100N. При N < 1000 первый алгоритм эффективнее. Но при N = 105 второй алгоритм в 100 раз быстрее первого. Нередко одна и та же задача может решаться разными по эффективности алгоритмами. Нахождение более быстрых алгоритмов часто ведет к прорыву в различных проблемах, как теоретических, так и практических. Например, быстрые алгоритмы работы со строками лежат в основе поисковых систем Интернета. Часто высокая скорость работы достигается путем использования рациональных структур данных. Так быстрый поиск информации в современных системах управления базами данных основан на Б-деревьях и хеш-таблицах. Рассмотрим разные подходы к решению задач, которые существенно отличаются по трудоемкости. Гомер Симпсон. Обеденный перерыв Гомера Симпсона составляет T миллисекунд. Один гамбургер Гомер съедает за N миллисекунд, один чизбургер - за M (1 ≤ M, N, T ≤ 2×109). Требуется найти максимальное суммарное число гамбургеров и чизбургеров, которые Гомер может съесть в течение обеденного перерыва. Предпочтителен вариант, когда дополнительного времени остаётся как можно меньше. Выдать остаток обеденного времени. Максимальное число гамбургеров составляет K = T div N, а чизбургеров L = T div M. Двойной цикл до K и L по трудоемкости вычислений нас устроить не может. В [2] указан следующий простой выход. Если съедается I гамбургеров, то число чизбургеров определяется как J = (T - I × N) div M. Значит, достаточно одинарного цикла. Но и такой подход не годится для заданной размерности. Пусть, например, M ≤ N. Очевидно, что максимальное суммарное число гамбургеров и чизбургеров составляет P = T div M. Как минимизировать остаток времени? Нужно вместо части гамбургеров съесть чизбургеры. Каждый из них потребует N - M из остатка времени. Значит, их можно съесть Q = (T - P × M) div (N - M). Тогда число гамбургеров уменьшится и составит P- Q. Окраска чисел. Профессор Психовецкий решил покрасить все числа от 1 до N (1 ≤ N ≤106 так, чтобы каждые два числа A и B имели разный цвет, если A делится на B. Каким минимальным количеством цветов можно обойтись? Ввод. В единственной строке задано число N. Вывод. В единственную строку вывести минимальное количество цветов. Примеры Ввод 1 Ввод 2 Ввод 3 3 5 12 Вывод 1 Вывод 2 Вывод 3 2 3 4 Можно решать задачу перебором, рассматривая по возрастанию возможные делители чисел до N включительно. Такой способ реализуется следующим образом.
Метод имеет квадратичную трудоемкость, поэтому непригоден при больших значениях N. Опишем другой подход. Греческому математику Эратосфену, жившему до нашей эры, принадлежит алгоритм нахождения простых чисел в заданном диапазоне. Из всех чисел последовательно удаляются те, которые делятся на 2, на 3 и т. д. В итоге остаются только простые числа. Такой способ называют методом «решета» [4]. Вот и нам в соответствии с этим методом более рационально перекрашивать последовательно все числа, имеющие общий делитель. Для этого достаточно переписать цикл окраски в следующем виде. For i:=1 to N div 2 do {i-первый сомножитель}
Пусть две подпоследовательности имеют одинаковую длину и их первые k элементов (k ≥ 0) совпадают. Будем считать лучшей ту из них, у которой больше k+1-й элемент. Эта подпоследовательность более перспективна для дополнения ее меньшими элементами. Снова начнем с обратного прохода от XN к X1, но в отличие от прежнего будем для каждого значения i искать лучшую подпоследовательность среди всех возрастающих подпоследовательностей, начинающихся с Xj при j ≥ i. Опишем массив XBeg размерности N. Величина XBeg[i] определяет значение первого элемента, которым начинается лучшая подпоследовательность длины i. Пусть L – текущая максимальная длина всех возрастающих подпоследовательностей. Тогда
Действительно, если есть лучшая возрастающая подпоследовательность длины M, начинающаяся с некоторого элемента, то после удаления ее первого элемента получим лучшую подпоследовательность длины M – 1, начинающуюся с большего элемента. Пусть XBeg[j+1] ≤ Xi < XBeg[j]. Это означает, что Xi может предшествовать лучшей подпоследовательности Sj длины j, но не может предшествовать лучшей подпоследовательности Sj+1 длины j+1. Тогда Sj с добавленным в начале Xi по крайней мере не хуже Sj+1, а первый элемент из Sj станет вторым после Xi в новой лучшей подпоследовательности длины j+1. Следовательно, нужно изменить XBeg[j+1] на Xi. Если j = L, то дополнительно увеличивается максимальная текущая длина L. При обратном проходе будут находиться подпоследовательности все большей длины, поэтому индекс начального элемента лучшей подпоследовательности длины M меньше индекса начального элемента лучшей подпоследовательности длины M - 1. Поэтому после завершения обратного прохода лучшую возрастающую подпоследовательность составят элементы XBeg[L], XBeg[L-1], ..., XBeg[1]. Источником ускорения работы является бинарный поиск номера j для каждого Xi. Трудоемкость бинарного поиска оценивается величиной O(Log L). Поскольку L ≤ N, то общая трудоемкость алгоритма составляет O(Nlog N). Приведем текст программы, реализующей описанный алгоритм. В отличие от [3], описанная модификация алгоритма позволяет обойтись единственным дополнительным массивом XBeg размерности N вместо трех массивов. Это позволяет в частности увеличить размерность N при реализации в Турбо Паскале даже по сравнению с первоначальным вариантом программы. Program Posled;
Премия. Жилищная контора решила наградить своего лучшего сантехника. Был составлен список балансовых сумм по N квартирам в виде целых чисел. Числа в списке пронумеровали от 1 до N. Известно, что должники имеют отрицательный баланс. Сантехник должен выбрать часть списка, указав начальный и конечный номера. Балансы, соответствующие этому диапазону номеров, суммируются. Полученное значение составляет величину премии сантехника. Требуется определить такие начальный и конечный номера, чтобы премия оказалась максимальной. Ввод из файла INPUT.TXT. Первая строка содержит значение N (1 ≤ N ≤ 100000). Во второй строке задаются через пробел балансы жильцов X1,…, XN (-1000 ≤ Xi ≤ 1000). Вывод в файл OUTPUT.TXT. В первой строке выводится максимальная сумма ожидаемой премии. Во второй строке выводятся начальный и конечный номера списка, соответствующие полученной сумме. Если ответов несколько, то указывается пара с минимальным начальным номером, а при одинаковых начальных номерах – пара с минимальным конечным номером. Ограничения. Время счета не более 1 с. Пример Ввод 10 4 -19 -4 2 21 -23 15 6 -2 5 Вывод 24 4 10
Проще всего в двойном цикле перебирать все пары с начальным и конечным номерами и для каждого интервала находить сумму чисел. Максимальная сумма даст ответ задачи. Оценим трудоемкость такого подхода. Двойной цикл в общем случае приводит к трудоемкости O(N2), а общая трудоемкость составит O(N3), что абсолютно неприемлемо. Более рационально при первом проходе накапливать в массиве S частичные суммы, то есть определять величины Si = X1+ X2+…+ Xi. Тогда Xi+ Xi+1+…+ Xj = Sj - Si-1. Такой подход обеспечивает трудоемкость O(N2), что также не может нас устроить. Оказывается, что существует линейный алгоритм с трудоемкостью O(N), который позволяет обходиться даже без массива, обрабатывая данные за один проход напрямую из файла [7]. Будем сохранять лучшую сумму элементов R на каждом шаге и соответствующий ей интервал индексов, а также текущую сумму элементов S. Если на очередном i-ом шаге встречается положительный элемент Xi, а S<0, то начнем отсчет с начала, положив S = Xi. В противном случае прибавим значение Xi к текущей сумме S. Сравним значения R и S. Максимальную из этих величин сохраним как лучшую сумму элементов R на i-ом шаге и перейдем к следующему шагу. Ниже приводится текст программы, реализующей этот алгоритм.
Вернемся для примера к задаче о черепашке, рассмотренной в разделе динамического программирования. Она сводится к нахождению кратчайшего пути из левого верхнего угла матрицы C размерности M×N, заполненной числами, в правый нижний. Путь состоит из переходов по матрице направо либо вниз. Ограничимся нахождением минимальной стоимости пути без определения самого пути. Очевидным решением является написание следующей рекурсивной функции, находящей минимальную стоимость пути от клетки матрицы с координатами (i, j) до конца.
Function F(i,j: integer): longint; Begin if (i=M) and (j=N) then F:=C[M,N] else if i=M then F:=C[i,j]+F(i,j+1) else if j=N then F:=C[i,j]+F(i+1,j) else if F(i+1,j)<F(i,j+1) then F:=C[i,j]+F(i+1,j) else F:=C[i,j]+F(i,j+1); End; Ответ дает значение функции F(1,1). Уже на матрице размерами 10×10 программа работает больше 20 секунд. Дело в том, что мы выполняем полный перебор путей, многократно пересчитывая значения функции для каждой клетки матрицы. С ростом размерности матрицы трудоемкость увеличивается экспоненциально.Можно, конечно, запоминать найденные значения функции в таблице, не пересчитывая их повторно. Но тем самым мы фактически снова придем к динамическому программированию. |
Loading
|