1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 1 из 6

Считаем точно: от кода к формуле T(n)

Коротко

Время алгоритма записывают не секундами, а функцией T(n) - сколько элементарных шагов он делает на входе размера n. Считают её построчно: у каждой строки своя цена и своё число выполнений, а T(n) - сумма произведений «цена × сколько раз». Секунды зависят от машины, число шагов - нет, поэтому считают шаги.

Если умеешь считать T(n) построчно и знаешь, что тест цикла выполняется на раз чаще тела - листай до «Теперь сам».

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

i = 0
while i < n:
    i = i + 1

Тело цикла выполнится n раз - это видно сразу. А сколько раз выполнится сама проверка i < n? Не n, а n + 1: последний раз она срабатывает, когда i уже дорос до n, возвращает ложь и выпускает из цикла. Один лишний раз - мелочь для этого примера, но именно на нём чаще всего ошибаются в точном счёте, а из таких мелочей и собирается формула.

На что это похоже

Счёт времени - это чек из магазина. В строке чека две колонки: цена за штуку и количество. Итог - не длина чека и не число позиций, а сумма произведений по всем строкам. Программа - тот же чек: у каждой строки кода своя цена (сколько шагов она стоит за одно выполнение) и своё количество (сколько раз выполнилась). T(n) - это итог по чеку.

Механизм: цена, число выполнений, сумма

Каждой строке приписывают цену c - число шагов за одно выполнение. Для строк из RAM-модели (арифметика, сравнение, присваивание, обращение по индексу) цена - константа: она не зависит от n. Дальше считают число выполнений каждой строки на входе размера n. T(n) - сумма произведений цены на число выполнений по всем строкам.

Возьмём сумму элементов списка:

s = 0                    # c1, один раз
for i in range(n):       # c2, n+1 раз (последняя проверка - вхолостую)
    s = s + a[i]         # c3, n раз
print(s)                 # c4, один раз
строка цена сколько раз
s = 0 c1 1
for i in range(n) c2 n + 1
s = s + a[i] c3 n
print(s) c4 1

Складываем произведения:

T(n) = c1·1 + c2·(n+1) + c3·n + c4·1
     = (c2 + c3)·n + (c1 + c2 + c4)

Получилась функция вида a·n + b - линейная. Все c - неизвестные константы, но их точные значения не нужны: важно, что при них n стоит в первой степени (то есть просто n, а не n·n = n²) - чем выше степень, тем круче растёт время. Это и есть ответ на вопрос «как растёт время»: вдвое больше данных - вдвое больше шагов.

Правило

T(n) - сумма по всем строкам произведения «цена строки × число её выполнений». Цена строки из RAM-модели - константа. Тело цикла выполняется столько раз, сколько кругов; проверка условия - на один раз больше тела.

Разбор: цикл с досрочным выходом

for i in range(n):
    if a[i] == x:
        print(i)
        break

Сколько шагов? Зависит от того, где стоит x. Если он первый - тело отработает один раз и break оборвёт цикл: константа. Если x в списке нет - тело отработает n раз: линия. Одна и та же программа даёт разный счёт на входах одного размера. Пока запомни сам факт; отдельная глава про худший и лучший случай разбирает, какой из этих счётов берут за ответ.

Разбор: та самая лишняя проверка

Вернёмся к разнице «тело против проверки». Посчитаем строку while в поиске минимума:

m = a[0]                 # 1 раз
i = 1                    # 1 раз
while i < n:             # n раз
    if a[i] < m:         # n-1 раз
        m = a[i]
    i = i + 1            # n-1 раз

Тело крутится n-1 раз (от i=1 до i=n-1), а проверка i < n - n раз: на единицу больше, последний прогон отправляет из цикла. В сумме это меняет только свободный член b, а не множитель при n, - но в точном счёте писать надо честно, иначе на вложенных циклах ошибка на единицу превратится в ошибку в целом слагаемом.

Что ломается без этого

Наивный счёт «одна строка - один шаг» ломается на строках, которые сами прячут цикл. if x in spisok - это не одна цена, а проход по списку, и в чеке у неё стоит не 1, а до n. Посчитав такую строку за единицу, ты объявишь квадратичную программу линейной и удивишься, почему она стоит на боевых данных (эту ловушку разбирал раздел про модель вычислителя). Поэтому цена строки - это не «строка», а работа, которую строка делает.

Зачем это в работе

T(n) считают до запуска, чтобы узнать, доживёт ли код до боевого объёма. Машина курса печатает не только результат, но и число шагов и таблицу роста: данных втрое - шагов втрое (линия) или вдевятеро (квадрат). Так формула с бумаги сверяется с замером, и расхождение сразу видно - обычно это спрятанный за коротким синтаксисом цикл.

Теперь сам

Посчитай T(n) для фрагмента (списки a и b длины n):

for i in range(n):
    for j in range(n):
        print(a[i] + b[j])

Ответ. Внешний for даёт n кругов, на каждом внутренний for делает n кругов, и на каждом из них print выполняется один раз - итого n·n = n² печатей. Проверки и служебные строки добавят слагаемые с меньшим n, но быстрее всех растёт n²: время квадратичное. Удвой данные - работы станет вчетверо больше.

Главное

T(n) - это время как функция от размера входа, и считают её построчно: цена строки (константа для операций RAM-модели) умножается на число выполнений, всё складывается. Тело цикла выполняется по разу на круг, проверка условия - на раз больше. Строка, прячущая цикл, стоит не единицу, а свою работу.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.