информатика
Карпов Юрий Глебович, Трифонов Петр Владимирович
СЛОЖНОСТЬ АЛГОРИТМОВ И ПРОГРАММ
КЛАССЫ СЛОЖНОСТИ АЛГОРИТМОВ
Важнейшими характеристиками эффективности программы являются время и память, необходимые для ее выполнения. Временная сложность программы показывает, как долго она будет выполняться, и обычно измеряется числом элементарных шагов решения. Эффективность программы, выполняемой на компьютере, находится в прямой зависимости от алгоритма, который эта программа реализует. Более того, можно показать [1], что при некоторых вполне разумных допущениях о правилах кодирования, выбранном языке программирования и реалистических моделях компьютеров эта эффективность по существу не зависит ни от языка, ни от способа кодирования, ни от быстродействия компьютера и является имманентной (внутренне присущей) только самому алгоритму. Такая же ситуация имеет место и в отношении памяти. Таким образом, проблема определения эффективности программы сводится к проблеме определения эффективности ее алгоритма. Как правило, для решения одной и той же задачи можно построить алгоритмы различной сложности. Задачей профессионального программиста при решении проблемы является не кодирование первого пришедшего в голову алгоритма, а нахождение и реализация такого алгоритма, который сможет наиболее эффективно решить проблему при ее практических размерах.
Пусть А - алгоритм решения некоторого класса проблем, а N - размерность отдельной проблемы из этого класса. Натуральное число N может быть, например, размерностью обрабатываемого массива, числом вершин обрабатываемого графа и т. д. Обозначим /А(^ функцию, дающую верхнюю границу максимального числа основных операций (сложения, умножения и т.д.), которые должен выполнить алгоритм А при решении задачи размерности N.
Будем говорить, что алгоритм А полиномиальный, если /А(Ю растет не быстрее, чем некоторый полином (в общем случае степенная функция) от N. Если /А(Ю растет быстрее любой степенной функции от N, будем называть алгоритм А экспоненциальным. Для некоторых задач удается построить алгоритмы, для которых /А(№) растет медленнее любой степенной функции от N. Подобные алгоритмы называются логарифмическими. Оказывается, что между классом полиномиальных и клас-
^ремеНАая слофАос&А праграмми ... обшАо и^мер-яейся гисмом злемеНйлрЯих шаго)& решения.
сом экспоненциальных алгоритмов есть очень существенная разница: при больших размерностях проблем (которые чаще всего и интересны на практике), полиномиальные алгоритмы могут быть выполнены на современных компьютерах, тогда как экспоненциальные алгоритмы для практических размерностей проблем часто не могут быть реализованы вовсе. Обычно решение проблем, порождающих экспоненциальные алгоритмы, связанно с перебором всех или почти всех возможных вариантов, и, ввиду невозможности реализации таких алгоритмов, для нахождения решения разрабатываются другие подходы. Например, даже если существует экспоненциальный алгоритм для нахождения оптимального решения некоторой проблемы, то на практике применяются другие, более эффективные алгоритмы для нахождения не обязательно оптимального, а только приемлемого (допустимого) решения. Обычно полиномиальный алгоритм может быть построен для нахождения (даже и не всегда оптимального) решения проблемы лишь тогда, когда удается глубоко проникнуть в суть этой проблемы.
Полиномиальные алгоритмы различаются в зависимости от степени полинома, аппроксимирующего /А(№). Рассмотрим сначала, как оценить временную сложность алгоритма. Такая оценка производится с использованием символа О («большого О»): говорят, что /А(№) растет как g(N) для больших N если существует положительная постоянная С >0, такая, что limN ® ¥ fА(N)/g(N) = С. Это записывается как ^(Ю = 0(я(Ю). Оценка называется временной асимптотической сложностью алгоритма. Оценка О^(^) для функции применяется, когда точное
значение неизвестно, а известен лишь порядок роста времени, затрачиваемого алгоритмом А на решение задачи размерностью N. Точные коэффициенты функции fА(N) зависят обычно от конкретной реализации, в то время как О^(^)) является характеристикой самого алгоритма. Например, если временная асимптотическая сложность алгоритма есть О(Ы2) (такой
алгоритм называется квадратичным), то при увеличении N время решения задачи увеличивается, как Факт экспоненциальной сложности алгоритма в терминах введенной символики можно записать так: fA(N) = 0(&), где к - некоторое число, большее единицы.
Важность исследования асимптотической сложности алгоритмов видна из следующего примера, приведенного в [2]. Пусть А, В, С и D - алгоритмы такой теоретической сложности: А - экспоненциальной О(2п), В - полиномиальной 0(п2), С - линейной О(п) и, наконец, D -логарифмической 0(^ п). Пусть пА, пв, пС и пв - максимальные размеры задач, соответствующих этим четырем алгоритмам, которые можно решить за разумное время (например, за несколько часов работы современного компьютера). Предположим, что изобретен новый компьютер, в тысячу раз более быстродействующий, чем существующие. Конечно, на этом компьютере можно решать задачи большей размерности, чем на существующем. Зададимся вопросом, каким будет этот выигрыш для алгоритмов с разной оценкой сложности.
• Для экспоненциального алгоритма А на новом компьютере можно решать задачи размерности на 10 единиц большие, чем пА.
• Для полиномиального алгоритма В можно теперь решать задачи в 30 раз большей размерности, чем пВ.
• Для линейного алгоритма С на новом компьютере можно решать задачи в 1000 раз более сложные, чем раньше.
• Для логарифмического алгоритма D можно теперь решать задачи размернос-
ПРИМЕР РАЗРАБОТКИ АЛГОРИТМОВ
РАЗЛИЧНОЙ СЛОЖНОСТИ ДЛЯ РЕШЕНИЯ ОДНОЙ ПРОБЛЕМЫ
Сравним алгоритмы различной сложности на примере одной интересной задачи, которая часто предлагается на собеседовании кандидатам при найме на
Листинг 1. Алгоритм 1
1 MaxSoFar = 0; 1
2 £ог L= 0; L<N; L+ + ) N
3 £ог U=L; ^^ ^ + ) { N + N - 1) + ... + 1 = N N + 1)/2
4 Sum = 0; N N + 1)/2
5 £ог (int i=L,• ^^ i + +) (1 + ... + N + (1 +...+ (N-1)) +...+ 1 = = S1=1N(N - 1 + 1)*^ - 1 + 2)/2
6 Sum += X[i],• - 1 + 1)*(N - 1 + 2)/2
7 MaxSoFar = max (MaxSoFar, Sum),•} N N + 1)/2
работу программистов (в частности, в компании Майкрософт). Сравнение алгоритмов решения этой задачи приведено в [3].
Исходными данными для задачи является вектор X из N целых чисел, выходом - максимальная сумма любого под-массива этого вектора. Например, если входной вектор есть X [0..9] = < 31, -41, 59, 26, -53, 58, 97, -93, -23, 84>, то результатом решения задачи является сумма подмассива X [2..6], которая равна 187. Это максимальная сумма среди всех возможных подмассивов массива X. Очевидным решением при всех положительных элементах вектора является сумма элементов всего входного вектора, а при всех отрицательных элементах - пустой под-массив с нулевой суммой.
Эта проблема возникает при построении системы распознавания образов: после представления изображения в цифровой форме степень наилучшего совпадения некоторого фрагмента этого изображения с заданным образцом определяется именно как максимальная сумма членов подмассивов массива, представляющего образ.
Очевидный алгоритм решения этой проблемы, который сразу возникает в голове неглубокого программиста таков. Для всех пар границ подмассивов Ь, и, таких, что 1 < L < и < N, где N - длина вектора X, подсчитать суммы членов и выбрать среди них максимальную (листинг 1).
Оценим сложность Алгоритма 1. В листинге справа от каждого оператора этого алгоритма подсчитано число его выполнений. Будем считать, что все опера-
торы одинаково сложны (если это не так, можно ввести соответствующий коэффициент). К оператору управления циклом (третья строка) производится N обращений; при первом обращении этот цикл выполняется N раз, при втором N - 1 раз, и т.д., всего N(N + 1)/2 раз. Столько же раз выполняются операторы 4 и 7. К выполнению оператора цикла 5 управление передается N + ^- 1) + (И- 2) + ... +1 раз. При первом обращении цикл выполняется повторно [1, 2, ..., N раз, затем [1, 2, ..., N - 1] раз и т.д. Всего операторы 5 и 6 выполняются
N
£ [(N - 1 +1)*( N - 1 + 2)/2] =
1=1
= N 3/6 + 2 N2 + 3N/4
раз, Общее число выполненных операторов алгоритма: /А1(М) = 1 + N + 3N(N - 1)/2 + + 2(^/6 + 2Ы2 + 3М4). Таким образом,
г> Р Р * 1 *
...сЯтеМ ^шлугмега сОкАреКсф Неко&лрлга фрггм&Н&А зЛого ч^аёрл^&Ния с ^л^лЯЯим образом...
Листинг 2. Алгоритм 2
1: MaxSoFar = 0; 1
2: for (int L=0; L<N; L++) { N
3: Sum = 0; N
4: for (int U=L; U<N; U++) { N + (N - 1) + (N - 2) + ... + 1 =
= N (N + 1)/2
5: Sum += X[U]; N (N + 1)/2
6: MaxSoFar = max (MaxSoFar, Sum);}} N (N + 1)/2
асимптотическая временная сложность этого Алгоритма 1 равна 0(N3).
Существует возможность сделать программу значительно быстрее. Более внимательный программист может заметить, что если уже вычислена сумма Sum элементов подмассива X [L ... U], то сумма элементов подмассива X [L ... U +1] получается из Sum просто добавлением элемента X [U +1]. На этой идее основан следующий алгоритм (листинг 2).
Общее число выполненных операторов Алгоритма 2: fA2(N) = 1 + 2N + 3N(N - 1)/2. Поэтому его асимптотическая временная сложность равна 0(N2).
Еще более проницательный разработчик программы может заметить, что прежде чем вычислять сумму элементов подмассива X [L ... U], стоит перестроить исходный массив X так, чтобы каждый его элемент содержал бы сумму всех предшествующих элементов массива X. Новый вспомогательный массив назовем CumArray. Таким образом, CumArray[i] будет равен X [1] + X [2] + ... + X [i]. Используя массив CumArray, вычислить сумму элементов подмассива X [L ... U] легко: она равна CumArray[U] - CumArray[L - 1]. Таким
образом, за одну операцию вычитания можно получить любую сумму, на получение которой в предыдущих алгоритмах тратилось 0(Ы) сложений. Новый алгоритм имеет следующий вид (листинг 3).
Асимптотическая временная сложность Алгоритма 3 равна О(Ы2), она такая же, как и у Алгоритма 2. Действительно, сложность первой вспомогательной части - получения вспомогательного массива - требует 0(N операций, а основная часть -это два вложенных цикла, и выполнение ее требует О(^) операций.
Возможность построения еще более эффективного алгоритма для решения этой проблемы представляется сомнительной: действительно, существует порядка О(Ы2) подмассивов у массива длины N, поэтому любой алгоритм, сравнивающий суммы всех этих подмассивов, с неизбежностью будет иметь сложность не ниже, чем О(Ы2).
Однако глубокий, думающий разработчик обычно не останавливается на достигнутом. Он всегда будет искать наиболее эффективное, элегантное решение поставленной проблемы. После глубокого анализа он может найти следующий прием.
Листинг 3. Алгоритм 3
MaxSoFar = 0;
CumArray[0] = 0;
for (int i=0; i<N; i++)
CumArray[i]:= CumArray[i-1] + X[i]; for (int L=1; L<N; L++) { for (int U=L; U<N; U++) {
Sum = CumArray[U] - CumArray[L-1]; MaxSoFar = max (MaxSoFar, Sum);}}
Пусть после анализа X [г], сканируя массив X слева направо, мы уже имеем значение Мах$о¥аг максимальной суммы среди всех подмассивов массива X [0 ... г]. Поставим вопрос: как можно найти значение МахБоЕаг среди всех подмассивов массива X [1 ... г + 1] после анализа следующего элемента X [г] массива X? Ключевой идеей здесь является следующее соображение: единственной альтернативой значению МахБоЕаг на следующем шаге является максимальная сумма всех таких подмассивов массива X, которые имеют правую границу точно в позиции г + 1. Назовем это значение МахЕпШщИеге. Если каким-то образом мы можем это значение вычислить на г + 1-м шаге, то новое значение Мах$о¥аг можно определить как максимальное среди двух значений: предыдущего значения МахБоЕаг и текущего значения МахЕп(ИщИете.
Можно подсчитывать значение МахЕп(ИщИете каждый раз в цикле, и тем самым мы можем построить новый алгоритм, временная сложность которого будет квадратичной. Но простые соображения показывают, что величину МахЕп(ИщИете можно получить итеративно из ее предыдущего значения и величины X [г + 1]. Таким образом, новый алгоритм имеет вид (листинг 4).
Временная сложность Алгоритма 4 равна О(Щ. Очевидно, что это наилучший возможный алгоритм, поскольку любой алгоритм обязательно должен просмотреть все N значений массива X хотя бы раз.
Приведенный пример демонстрирует часто возникающую ситуацию: тщательное продумывание задачи ведет к уменьшению сложности решающего ее алгоритма, а это, в свою очередь, дает существенное уменьшение времени решения. Так, например,
время решения этой проблемы для входного массива X длиной 107 на машине Dell XPS M1710 для первого алгоритма - несколько недель, а для последнего - менее секунды.
КРИПТОГРАФИЯ: ПРАКТИЧЕСКОЕ ИСПОЛЬЗОВАНИЕ
РЕЗУЛЬТАТОВ ТЕОРИИ СЛОЖНОСТИ АЛГОРИТМОВ
Теория сложности кажется довольно абстрактной ветвью информатики, имеющей весьма небольшое прикладное значение. Однако существуют целые разделы практической информатики, целиком основанные на результатах этой теории. Одним из таких разделов является крип-тозащита в компьютерных сетях.
ШИФРОВАНИЕ С ОТКРЫТЫМ КЛЮЧОМ
Много лет люди были уверены, что защита от несанкционированного доступа при обмене конфиденциальной информацией может быть организована только в случае, если обе обменивающиеся информацией стороны разделяют один и тот же секретный ключ. Для компьютерных
.oftifr te фе сек^еЛЯий к*тг,
Листинг 4. Алгоритм 4
MaxSoFar = 0;
MaxEndingHere = 0;
for (int i=0; i<N; i++) {
MaxEndingHere = max (0, MaxEndingHere + X[i]); MaxSoFar = max (MaxSoFar, MaxEndingHere);}}
сетей, где обычным является оперативно организуемый обмен конфиденциальной информацией между абонентами, никогда друг друга не видевшими, встала следующая проблема: как двум сторонам договориться об общем секретном ключе, как передать сам ключ по сети? Открытая передача секретного ключа в каналах связи сопряжена с риском потери секретности при его перехвате. Поэтому ключ для передачи тоже надо зашифровать с помощью нового ключа. Как разорвать этот порочный круг?
В 1976 г. было обнаружено, что можно построить обмен конфиденциальной информацией на основе так называемого «открытого ключа», без передачи общего для двух сторон секретного ключа. Введем две функции: функцию (ключ) шифрования Е и обратную ей функцию (ключ) дешифрования D. Как правило, общий вид этих функций общеизвестен, а секретность обеспечивается введением соответствующих параметров. Если М - сообщение, то Е(М) - зашифрованное сообщение, и D(Е(M)) - дешифрированное сообщение, то есть М. Идея криптографии с открытым ключом состоит в том, что если по известной функции Е очень трудно (практически невозможно) найти обратную ей функцию D, то пользователь А может опубликовать свою функцию Е, храня функцию D в секрете. Любой может направить пользователю А зашифрованную информацию, воспользовавшись открытой функцией Е, но только сам А может
фассмо&рим. схему
оазиса.
расшифровать ее, используя свой секретный ключ D.
Слова «трудно (практически невозможно) найти» имеют здесь смысл именно алгоритмической трудности поиска, а не трудности розыскных мероприятий. Достаточно быстро криптографы отказались от таких шифров, как, например, подстановочные (как в «Пляшущих человечках» А. Конан-Дойля, где каждая буква текста заменяется специальным знаком). Подстановочные шифры легко раскрываются вычислением частотного словаря зашифрованного сообщения. В настоящее время криптография рекомендует системы шифрования, для которых сложность всех известных в настоящее время атак достаточно велика. Шифр считается надежным, если затраты на взлом (выражаемые в объеме необходимых компьютерных ресурсов) превышают выигрыш от получения зашифрованной информации. С течением времени разрабатываются новые атаки, совершенствуется вычислительная техника, что приводит к снижению криптос-тойкости систем шифрования. Для обеспечения адекватной защиты приходится или пересматривать параметры криптосистем (например, увеличивать длину ключа), или разрабатывать новые криптосистемы.
Как правило, при передаче больших объемов данных собственно их шифрование производится с помощью некоторого симметричного шифра, а ключ шифрования защищается с помощью криптосистемы с открытым ключом. Это позволяет существенно снизить сложность вычислений.
ЭЛЕКТРОННАЯ ПОДПИСЬ
Рассмотрим схему для удостоверения сообщений - электронной подписи. Очевидно, что если для функций Е и D выполняется D(Е(M)) = М, то справедливо и Е(Э(М)) = М, то есть функции D и Е взаимно обратны. Пусть А и В - два корреспондента, с соответствующими ключами шифрования и дешифрования ЕА, БА, Ев,
DB и пусть Еа и Ев - открыты. Тогда А «подписывает» свое сообщение М, направляемое к В, следующим образом. Он шифрует М, используя сначала свой секретный ключ DA, а затем получившийся зашифрованный код еще раз шифруется открытым ключом EB с получением закодированного сообщения Eb(Da(M)). Это сообщение может быть дешифровано только корреспондентом В, имеющим свой секретный ключ дешифрования DB, так: Db(Eb(Da(M))) с получением DA(M). При этом В уверен, что сообщение М пришло именно от А, если он может полученный код Da(M) дешифровать открытым ключом для А: Ea(Da(M)) = М. Необходимо отметить, что электронная подпись может использоваться и без шифрования.
ПРИМЕР КРИПТОСИСТЕМЫ С ОТКРЫТЫМ КЛЮЧОМ
Рассмотрим один из простейших примеров решения проблемы криптозащиты с открытым ключом. Пусть a и b - заданные параметры-константы, z - переменная, и h(z) = az mod b - функция от a, b и z. Очевидно, что вычисление этой функции не представляет трудностей, она имеет линейную сложность по отношению к размеру z. В то же время вычисление обратной функции, а именно, определение z при заданных a, b и x = h(z) является трудным делом. Эта задача носит название проблемы дискретного логарифма. В настоящее время отсутствуют полиномиальные алгоритмы решения этой задачи. Это значит, что увеличивая размеры параметров можно сделать проблему нахождения обратной функции для h(z) практически неразрешимой.
Эти результаты могут быть использованы в криптозащите, например, следую -щим образом. Пусть банк и его клиенты имеют одни и те же известные им всем параметры a и b. Банк должен организовать обмен конфиденциальной информацией со своими клиентами. Любой кли-
ент А банка имеет свой уникальный1 секретный код х. Этот клиент при регистрации посылает в банк не сам секретный код х, а значение функции h(x) = ax mod b, которое и хранится в банке.
Рассмотрим протокол Диффи-Хеллма-на, позволяющий организовать построение так называемого общего разделяемого ключа для одной сессии обмена конфиденциальной информацией. Для этого банк генерирует случайное число у и вычисляет функцию h(y). Именно этот код посылается от банка клиенту А как открытый ключ. Клиент, получив значение h(y) = ay mod b, использует свое секретное число x для того, чтобы вычислить K = (h(y))x = axy mod b. Банк находит это же значение как (h(x))y. Эта величина используется далее в качестве ключа шифрования некоторого симметричного шифра.
Защита передаваемой информации обеспечивается здесь тем, что общий секрет К не передается открыто, и даже имея «открытые ключи» h(y) и h(x), величину К можно построить только при знании персонального ключа х клиента или же числа у, которые не передаются открыто. На стороне банка секретный ключ х неизвестен, и для обеспечения защиты информации со стороны нечестных сотрудников банка, имеющих доступ к значению h(x), следует только скрыть (забыть, стереть) сразу после генерации случайное число у. Конечно, сам ключ К должен быть также уничтожен сразу после сессии -кодирования сообщения М. Для этого можно разработать защищенную аппаратуру, которая будет генерировать случайную величину у, вычислять функцию h(y) и сразу же после получения кода К текущей сессии стирать сгенерированное значение у.
Именно отсутствие эффективных алгоритмов получения z по h(z), то есть решения задачи дискретного логарифма, дает возможность организации такой схемы криптозащиты.
1 Иногда величина x генерируется случайным образом.
КРИПТОСИСТЕМА RSA
Реально используемая в настоящее время криптографическая система с открытым ключом RSA, названная так по фамилиям ее разработчиков (Rivest, Shamir, Adleman, 1978), также построена на основе алгоритмически сложных проблем теории чисел. Здесь функции шифрования Е: Е(М)= Ме mod n и дешифрования D: D(P) = Pd mod n строятся так:
- выбираются два больших простых числа p и q;
- вычисляются n = pq и z = (p - 1)(q - 1);
- выбирается число d, взаимно простое с z;
- подбирается е, такое, что de =1 mod z.
Доказано, что М = (Ме mod(n))d mod(n),
то есть две функции - Е и D - взаимно обратны и могут использоваться для криптографической защиты с открытым ключом. При шифровании исходный текст (рассматриваемый как строка битов) разбивается на блоки по k бит каждый так, чтобы 2k < n. Для шифрования блока М вычисляется Мк = Е(М). Для дешифрования блока Мк вычисляется D^k) = М. Для шифрования необходима пара чисел е и n, для дешифрования нужны d и n. Поэтому открытый ключ состоит из пары (е, n), а закрытый - из пары (d, n).
Литература
Рассмотрим пример. Выберем p = 3, q =11, что дает n = 33 и z = 20. Выберем d = 7, как простое к 20. Решая сравнение 7e =1 mod 20, найдем e = 3. Таким образом, каждый шифрованный блок Р по исходному блоку М строится по правилу Р = М3 mod 33, а дешифрирование каждого блока М проводится по формуле М = Р7 mod 33. В этом простейшем примере независимо шифруются блоки из 5 битов, поскольку 25 < n.
Для вскрытия шифра необходимо разложить опубликованное число n на множители. Защита передаваемой информации основана на том, что существующие алгоритмы разложения на множители имеют высокую сложность, хотя невозможность построения более эффективных алгоритмов не доказана формально. Попытки математиков в течение почти 300 лет найти полиномиальный алгоритм такого разложения оказались безуспешными.
В заключение можно сказать, что фактически все проблемы защиты информации в компьютерных сетях связаны с поиском специфических вычислительных проблем, имеющих очень высокую сложность (и, конечно, обладающих рядом дополнительных свойств).
1. М. Гэри, Д. Джонсон. Вычислительные машины и труднорешаемые задачи. М.: «Мир», 1982.
2. А. Ахо, Дж. Хопкрофт, Дж. Ульман. Построение и анализ вычислительных алгоритмов. М.: «Мир», 1979.
3. J. Bentley. Algorithm design techniques // Communications of the ACM. V. 27, № 9, 1984.
4. С. Гудман, С. Хидетниеми. Введение в разработку и анализ алгоритмов. М.: «Мир», 1981.
Карпов Юрий Глебович, доктор технических наук, профессор, заведующий кафедрой «Распределенные вычисления и компьютерные сети» СПбГПУ,
Трифонов Петр Владимирович, кандидат технических наук, доцент кафедры «Распределенные вычисления и компьютерные сети» СПбГПУ.
© Наши авторы, 2007 Our authors, 2007