Для анализа алгоритма необходимо ответить на два важных вопроса: «Правильно ли он работает?» и «Сколько времени занимает его выполнение?». В этом параграфе мы познакомимся с характеристиками алгоритмов и задач, которые они решают.

Что такое алгоритм?

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

Когда вы закончили проектировать алгоритм, необходимо ответить на два важных вопроса: «Правильно ли он работает?» и «Сколько времени занимает выполнение?». Разумеется, вас не устроит алгоритм, который выдаёт правильный результат лишь в половине случаев или требует лет для поиска ответа.

Псевдокод

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

Задача и экземпляр задачи

Задача описывает класс возможных входных данных. Экземпляр задачи — это один конкретный ввод такого класса. Чтобы продемонстрировать понятия задачи и экземпляра задачи, рассмотрим следующий пример. Вы оказались в книжном магазине и собираетесь купить книгу за $, расплатившись купюрой в $. Вам должны вернуть центов в качестве сдачи. Теперь кассир принимает решение, как именно это сделать. Согласитесь, неприятно получить горсть из пенни или никелей и пенни. Возникает вопрос: как выдать сдачу, не расстроив клиента? Большинство кассиров стараются уместить сумму сдачи в наименьшее количество монет.

💡 Остановитесь и подумайте:
Каково минимальное количество монет номиналом , необходимо для сдачи в центов?

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

Задача «Размен»

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

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

Кассиры по всему миру решают эту проблему с помощью простого алгоритма:

Change(money, c, d):
    while money > 0:
        coin = ... // монета с самым большим номиналом, который не превышает money
        // дать монету с номиналом coin клиенту
        money = money - coin

Вот быстрая версия Change:

Change(money, c, d):
    for k in range(1, d + 1) 
        i_k = floor(money / c[k]) // наибольшее количество монет номинала c[k]
        // дать i_k монет с номиналом c[k] клиенту
        money = money - c[k] * i_k

Корректные и некорректные алгоритмы

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

💡 Остановитесь и подумайте:
Каково минимальное количество монет номиналами , необходимое для сдачи в центов?

Change — это некорректный алгоритм! Представьте сдачу в 40 центов, выданную в номиналах , , , и . Change привел бы к неправильному результату: он выдал бы 1 четвертак (25 центов), 1 дайм (10 центов) и 1 никель (5 центов) вместо 2 монет по двадцать центов. Хоть это и может выглядеть надуманно, в 1875 году в США существовала монета в двадцать центов. Насколько мы можем быть уверены, что Change выдаст минимальное количество монет в современных номиналах Соединенных Штатов или любой другой страны?

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

BruteForceChange(money, c, d):
    smallestNumberOfCoins = ∞
    for each combinations of coins (i_1,...,i_d)
    // от (0,...,0) до (money/c[1],...,money/c[d])
        valueOfCoins = ∑ i_k*c_k // сумма по всем k от 1 до d
        if valueOfCoins = money:
            numberOfCoins = ∑ i_k // суммарное количество монет
            if numberOfCoins < smallestNumberOfCoins:
                smallestNumberOfCoins = numberOfCoins
                change = (i_1, i_2, ... ,i_d)
    return change

Цикл повторяется с каждой возможной комбинацией из индексов и останавливается, когда достигает

Как мы можем узнать, что BruteForceChange не содержит ту же проблему, что и Change, — неверный результат при каком-то вводе? Раз BruteForceChange рассматривает все возможные комбинации номиналов, рано или поздно алгоритм придёт к оптимальному решению и запишет его в массив . В любой комбинации монет, которая даёт в сумме , должно быть как минимум столько же монет, сколько и в оптимальной. Таким образом, BruteForceChange никогда не завершит работу с неоптимальным набором .

На данный момент мы ответили только на один из двух важных вопросов об алгоритмах: "Работает ли он?". Однако мы не ответили на вопрос: "Сколько времени занимает выполнение?".

💡 Остановитесь и подумайте:
Сколько примерно итераций цикла for выполняет BruteForceChange?

Быстрые и медленные алгоритмы

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

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

Представьте, что алгоритм выполняет операций при вводе размера , и алгоритм решает ту же задачу за операций. Какой алгоритм быстрее: или ? Хотя и может быть быстрее, чем , при более малом значении (например, при между 1 и 3), будет быстрее при больших значениях (например, ). См. рис.. Так как — это, в каком-то смысле, более «быстрорастущая» функция относительно , чем . При этом константы 3 и 2 в не влияют на конкуренцию между двумя алгоритмами при больших значениях . Мы называем квадратичным алгоритмом и линейным. менее эффективен, чем , потому что он выполняет больше операций для решения задачи, когда значение большое. Так, иногда мы будем допускать неточности при подсчете операций алгоритма: поведение алгоритма при маленьком вводе неважно.

Group

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

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

Например, алгоритм с временем выполнения (линейный), (квадратичный), (кубический) или даже будет полиномиальным. Конечно, алгоритм с временем выполнения не очень практичен. Возможно, даже менее практичен, чем некоторые экспоненциальные алгоритмы. Впрочем, разработчики тратят много усилий, чтобы проектировать всё более и более быстрые полиномиальные алгоритмы. Раз значение может быть большим при вызове алгоритма с большим количеством номиналов (например, ), мы видим, что выполнение BruteForceChange может потребовать много времени.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

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

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

Следующий параграф3.1. Полный перебор и оптимизация перебора

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

Курс «Алгоритмы и структуры данных»

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