Центральный Дом Знаний - Лекции по структурам и алгоритмам обработки данных (СиАОД) 12

Информационный центр "Центральный Дом Знаний"

Заказать учебную работу! Жми!



ЖМИ: ТУТ ТЫСЯЧИ КУРСОВЫХ РАБОТ ДЛЯ ТЕБЯ

      cendomzn@yandex.ru  

Наш опрос

Как Вы планируете отдохнуть летом?
Всего ответов: 922

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Форма входа

Логин:
Пароль:

Лекции по структурам и алгоритмам обработки данных (СиАОД) 12

Лекции по структурам и алгоритмам обработки данных (СиАОД)

стр.: 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25

Полный текст лекций можно скачать здесь

10. Трудоемкость алгоритмов


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

Рассмотрим наглядный пример. Пусть требуется найти кратчайший путь на графе из 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 ≤ MNT ≤ 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 ≤ N106 так, чтобы каждые два числа A и B имели разный цвет, если A делится на B. Каким минимальным количеством цветов можно обойтись?

Ввод. В единственной строке задано число N.

Вывод. В единственную строку вывести минимальное количество цветов.

Примеры

Ввод 1 Ввод 2 Ввод 3

3 5 12

Вывод 1 Вывод 2 Вывод 3

2 3 4

Можно решать задачу перебором, рассматривая по возрастанию возможные делители чисел до N включительно. Такой способ реализуется следующим образом.


Program Paint;

Const Max=1000000;

Var

M,N,i,j: longint;

Color: array [1..Max] of byte; {номера цветов}

Fin,Fout: text;

begin

Assign(Fin,'input.txt');

Reset(Fin);

ReadLn(Fin,N);

Close(Fin);

For i:=1 to N do Color[i]:=1; {инициализация}
M:=1; {число цветов и максимальный номер цвета}
For i:=2 to N do {i-число для окраски}
For j:=1 to i div 2 do {j-предполагаемый делитель}

if (i mod j = 0) and (Color[i]=Color[j]) then

begin

Color[i]:=Color[i]+1;

if Color[i]>M then M:=M+1;

end;

Assign(Fout,'output.txt');

Rewrite(Fout);

WriteLn(Fout,M);

Close(Fout);
End.

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

Греческому математику Эратосфену, жившему до нашей эры, принадлежит алгоритм нахождения простых чисел в заданном диапазоне. Из всех чисел последовательно удаляются те, которые делятся на 2, на 3 и т. д. В итоге остаются только простые числа. Такой способ называют методом «решета» [4].

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

For i:=1 to N div 2 do {i-первый сомножитель}
For j:=2 to N div i do {j-второй сомножитель}

Begin

k:=i*j;
if Color[k]=Color[i] then Color[k]:=Color[i]+1;

if Color[k]>M then M:=Color[k];

end;
Вернемся к классической задаче динамического программирования о возрастающей подпоследовательности, рассмотренной выше. Напомним кратко ее постановку. Требуется из N целых чисел X1, X2, ..., XN вычеркнуть минимальное количество чисел так, чтобы оставшиеся шли в порядке возрастания. В описанном выше подходе для каждого значения Xi находилась максимальная возрастающая подпоследовательность, начинающаяся с этого значения. Легко видеть, что трудоемкость алгоритма составляет O(N2). При N = 107 и скорости вычислений 109 итераций в секунду для вычислений потребуется 105 секунд или более суток. Известен алгоритм, имеющий трудоемкость O(Nlog N) [3]. При N = 107 величина Nlog N не превышает 3108, то есть задача может быть решена в приемлемое время.

Пусть две подпоследовательности имеют одинаковую длину и их первые k элементов (k 0) совпадают. Будем считать лучшей ту из них, у которой больше k+1-й элемент. Эта подпоследовательность более перспективна для дополнения ее меньшими элементами.

Снова начнем с обратного прохода от XN к X1, но в отличие от прежнего будем для каждого значения i искать лучшую подпоследовательность среди всех возрастающих подпоследовательностей, начинающихся с Xj при ji.

Опишем массив XBeg размерности N. Величина XBeg[i] определяет значение первого элемента, которым начинается лучшая подпоследовательность длины i.

Пусть L – текущая максимальная длина всех возрастающих подпоследовательностей. Тогда

XBeg[1] > XBeg[2] > ...> XBeg[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). Поскольку LN, то общая трудоемкость алгоритма составляет O(Nlog N).

Приведем текст программы, реализующей описанный алгоритм. В отличие от [3], описанная модификация алгоритма позволяет обойтись единственным дополнительным массивом XBeg размерности N вместо трех массивов. Это позволяет в частности увеличить размерность N при реализации в Турбо Паскале даже по сравнению с первоначальным вариантом программы.

Program Posled;
Const
R=100000;
{максимальный размер последовательности для Turbo Pascal
около 16000 на числах типа word}
Var
N: LongInt; {заданный размер последовательности}
i,j,Max: LongInt;
{Max - текущая длина наилучшей подпоследовательности}
X: array[1..R] of LongInt;

XBeg: array[1.. R] of LongInt;

{XBeg[i] - значение начального элемента лучшей возрастающей
подпоследовательности, имеющей длину i:
XBeg[1] > XBeg[2] > ...> XBeg[Max] }

Left,Right,Mid: LongInt;

Fin,Fout: text;

Begin

Assign(Fin,'input.txt');

Reset(Fin);

ReadLn(Fin,N);

For i:=1 to N do Read(Fin,X[i]);

Close(Fin);

XBeg[1]:=X[N];

For i:=2 to N do XBeg[i]:=0;

Max:=1; {наибольшая длина среди всех подпоследовательностей}
For i:=N-1 downto 1 do

begin

Left:=0; Right:=Max+1;
{поиск j такого, что XBeg[j+1] <= X[i] < XBeg[j] }

While Right-Left > 1 do

begin

Mid:=(Right+Left) div 2;

if X[i] < XBeg[Mid] then Left:=Mid

else Right:=Mid;

end;

j:=Left;

if j=Max then {X[i] удлиняет максимальную подпоследовательность}
begin

Max:=Max+1;

XBeg[Max]:=X[i];

end
else {X[i] замещает начало максимальной подпоследовательности}
XBeg[j+1]:=X[i];

end;

Assign(Fout,'output.txt');

Rewrite(Fout);

WriteLn(Fout,Max);

For i:=Max downto 1 do

Write(Fout,XBeg[i],' ');

Close(Fout);
End.
Для решения следующей задачи также рассмотрим алгоритмы, существенно отличающиеся по трудоемкости.

Премия. Жилищная контора решила наградить своего лучшего сантехника. Был составлен список балансовых сумм по 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-ом шаге и перейдем к следующему шагу. Ниже приводится текст программы, реализующей этот алгоритм.


Program BentFile; { линейный алгоритм }

Var

N,i,j,X,R,S,Res: LongInt;

{R- максимальная сумма на предыдущем шаге, S-текущая сумма} Fin,Fout: text;
Ra,Rb,Sa,Sb,a,b: LongInt;
{диапазоны индексов, обеспечивающие максимальные суммы для R,S и общую максимальную сумму }

Begin

Assign(Fin,'input.txt');

Reset(Fin); ReadLn(Fin,N);

R:=-MaxLongInt; S:=0;

Sa:=1; Sb:=1; Ra:=1; Rb:=1; a:=1; b:=1;

For i:=1 to N do

begin

Read(Fin,X);

if (S<0) and (X>=0) then

begin

Sa:=i; S:=X

end

else S:=S+X;

Sb:=i;

if X>R then

begin

Ra:=i; Rb:=i; R:=X

end;

if S>R then

begin

Res:=S; a:=Sa; b:=Sb;

Ra:=Sa; Rb:=Sb; R:=Res;

end

else

begin

a:=Ra; b:=Rb; Res:=R;

end;

end;

Close(Fin);

Assign(Fout,'output.txt'); Rewrite(Fout);

WriteLn(Fout,Res);

WriteLn(Fout,a,' ',b);

Close(Fout);
End.
Многие задачи легко программируются с помощью рекурсии. И в этом случае необходима оценка трудоемкости алгоритмов.

Вернемся для примера к задаче о черепашке, рассмотренной в разделе динамического программирования. Она сводится к нахождению кратчайшего пути из левого верхнего угла матрицы 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

Календарь

«  Май 2024  »
ПнВтСрЧтПтСбВс
  12345
6789101112
13141516171819
20212223242526
2728293031

Архив записей

Друзья сайта

  • Заказать курсовую работу!
  • Выполнение любых чертежей
  • Новый фриланс 24