# теория · шаг 1 из 7
От просьбы к спецификации: вход, выход и что значит «лучше»
Коротко
Задачу почти никогда не дают спецификацией - её дают просьбой: «найди подозрительных пользователей», «сделай расписание получше». Первая работа разработчика - превратить просьбу в спецификацию из раздела 1: что именно на входе, что считается ответом и какое свойство его проверяет. Для этого есть короткий список вопросов: из чего состоит вход и какого он размера, какой вид ответа нужен (есть ли / сколько / все / лучший), что значит «лучше» и устроит ли почти лучший. Одна и та же фраза при разных ответах на эти вопросы - разные задачи с разной ценой: в разборе ниже два честных толкования «слишком часто» дают разные списки нарушителей на одном журнале.
Если ты привык уточнять вход, вид ответа и критерий до первой строки кода, листай до «Теперь сам».
Как думаешь, кого здесь надо найти
Безопасник просит: «Найди пользователей, которые заходят слишком часто - больше четырёх раз за час». Вот журнал за утро, минуты от полуночи:
ann: 5, 20, 40, 59
bob: 10, 55, 57, 58, 61, 64
Кто из двоих нарушитель? Ответь себе до чтения.
Если «за час» - это календарный час с 0:00 до 0:59, у Боба там четыре входа (10, 55, 57, 58) и ещё два в следующем часе: никто не нарушил. Если «за час» - любые шестьдесят минут подряд, то с 10-й по 69-ю минуту у Боба шесть входов: нарушитель. Оба толкования честные, а ответы разные. Программа, написанная по одному из них, будет «правильной» и при этом не той, что нужна.
На что это похоже
Заказ у портного. «Сшейте костюм, чтобы хорошо сидел» - это просьба. Портной не берёт ножницы, пока не снял мерки, не выяснил, к какому дню и сколько можно потратить. Мерки - это спецификация, и снимают их вопросами, а не догадками: догадку исправляют на примерке, когда ткань уже разрезана.
Механизм: пять вопросов к любой просьбе
Скиена в главе «Как разрабатывать алгоритмы» начинает список вопросов разработчика с одного: «понимаю ли я задачу?» - и раскладывает его на подвопросы. Из них вырастает рабочий порядок:
- Из чего состоит вход? Какие объекты (числа, строки, пары, события со временем), есть ли повторы, упорядочен ли он, что гарантируется (предусловие).
- Какой вид ответа? Здесь четыре разные задачи прячутся за одной фразой: - есть ли такой объект - ответ «да» или «нет»; - сколько их - одно число; - все такие объекты - список, и он может быть огромным; - лучший объект по какому-то критерию - и тогда нужен сам критерий.
- Что значит «лучше»? Минимизировать что - сумму, максимум, число шагов? При равенстве какой брать? Нужен ли точный оптимум, или устроит почти лучший - и насколько «почти»? Это записывают числом в постусловие: «сумма не больше чем на 5% выше оптимума». Оптимум при этом обычно неизвестен, и обещание проверяют либо через доказанную нижнюю границу оптимума (так устроены приближённые алгоритмы раздела 22), либо перебором на маленьких входах.
- Каков размер? Десять элементов, тысяча или миллиард - от этого зависит, какой алгоритм вообще допустим (следующий урок).
- Какие края? Пустой вход, один элемент, все одинаковые, касающиеся интервалы, равные времена.
Каждый ответ ты записываешь в спецификацию: вход и предусловие, выход, постусловие. Вопрос без ответа - это место, где две программы разойдутся.
Правило
Прежде чем выбирать алгоритм, запиши вход, вид ответа (есть ли / сколько / все / лучший), критерий «лучше» и размер. Если на какой-то вопрос нет ответа - задай его заказчику; если заказчика нет - выбери толкование явно и напиши его в спецификации.
Разбор: два толкования «слишком часто»
Запишем оба толкования из начала урока как программы - это и есть спецификации, только исполняемые.
# журнал входов: [пользователь, минута от полуночи]
zhurnal = [["ann", 5], ["bob", 10], ["ann", 20], ["bob", 55],
["bob", 57], ["bob", 58], ["ann", 40], ["bob", 61],
["bob", 64], ["ann", 59]]
K = 4 # «слишком часто» - больше K входов
W = 60 # за W минут
def po_chasam(zhurnal):
# толкование 1: календарные часы 0:00-0:59, 1:00-1:59, ...
schet = {}
for z in zhurnal:
klyuch = z[0] + " в час " + str(z[1] // 60)
schet[klyuch] = schet.get(klyuch, 0) + 1
plohie = []
for klyuch, n in sorted(schet.items()):
if n > K:
plohie.append(klyuch)
return plohie
def skolzyashchee(zhurnal):
# толкование 2: любое окно [t, t + W), где t - время какого-то входа
vremena = {}
for z in zhurnal:
if z[0] not in vremena:
vremena[z[0]] = []
vremena[z[0]].append(z[1])
plohie = []
for imya, t in sorted(vremena.items()):
t.sort()
levyj = 0
for pravyj in range(len(t)):
while t[pravyj] - t[levyj] >= W:
levyj = levyj + 1
if pravyj - levyj + 1 > K:
plohie.append(imya + " с минуты " + str(t[levyj]))
break
return plohie
# первое толкование отвечает парами «пользователь, час»,
# второе - пользователем и началом окна: даже вид ответа разный
print("по календарным часам:", po_chasam(zhurnal))
print("скользящим окном: ", skolzyashchee(zhurnal))
по календарным часам: []
скользящим окном: ['bob с минуты 10']
Обе программы правильные - каждая для своей задачи. Заметь, что второе толкование потребовало ещё двух
решений, которых просьба не содержала: окно полуоткрытое (t[pravyj] - t[levyj] >= W выталкивает вход,
случившийся ровно через 60 минут), а окна начинаются только в моменты входов. Второе не меняет списка
нарушителей (хотя начало окна программа называет по первому входу в нём, а не по самой ранней минуте): если
в каком-то окне больше K входов, то в окне, начатом с первого из них, их не меньше. Но это надо было
заметить и записать, а не получить случайно.
Что с этим делать на практике? Спросить. Вопрос «календарный час или любые 60 минут?» занимает одну строку в переписке, а неверный ответ на него - переписанный модуль и пропущенная атака.
Разбор: одна фраза - четыре задачи разной цены
«Найди пары чисел с суммой 11». В списке двести случайных чисел от 1 до 10; пара - это две позиции i < j,
и равные числа на разных позициях дают разные пары. Посмотрим, что стоит каждый
вид ответа.
import random
random.seed(7)
a = []
for i in range(200):
a.append(random.randint(1, 10))
S = 11
# 1. есть ли пара с суммой S: хватит множества виденных
vidennye = set()
shagov = 0
est = False
for x in a:
shagov = shagov + 1
if S - x in vidennye:
est = True
break
vidennye.add(x)
print("есть ли: ", est, "- просмотрено", shagov, "чисел")
# 2. сколько пар: счётчики значений, ответ - одно число;
# x образует пару с каждым уже встреченным S - x, по одной на каждого
schet = {}
par = 0
for x in a:
par = par + schet.get(S - x, 0)
schet[x] = schet.get(x, 0) + 1
print("сколько: ", par, "- просмотрено", len(a), "чисел")
# 3. все пары: ответ сам по себе такой длины
vse = []
for j in range(len(a)):
for i in range(j):
if a[i] + a[j] == S:
vse.append([i, j])
print("все пары: ", len(vse), "пар в ответе")
# 4. ближайшая сумма, если точной нет: сортировка и два указателя
b = sorted(a)
i = 0
j = len(b) - 1
luchshaya = b[0] + b[1]
# ответ «лучший объект» - сама пара, а не только её сумма
para = [b[0], b[1]]
while i < j:
s = b[i] + b[j]
if abs(s - 22) < abs(luchshaya - 22):
luchshaya = s
para = [b[i], b[j]]
if s < 22:
i = i + 1
else:
j = j - 1
print("ближе к 22:", luchshaya, "=", para[0], "+", para[1])
есть ли: True - просмотрено 6 чисел
сколько: 1969 - просмотрено 200 чисел
все пары: 1969 пар в ответе
ближе к 22: 20 = 10 + 10
«Есть ли» закончился на шестом числе: 2 и 9 нашлись быстро. «Сколько» прошёл весь список один раз и выдал одно число. «Все пары» - другая порода: в ответе 1969 пар, и никакой алгоритм не выпишет их быстрее, чем за 1969 действий. Размер ответа - нижняя граница времени, и если в списке из миллиона чисел пар много, этот ответ не поместится в память, сколько ни оптимизируй поиск. Четвёртый вид появляется, когда точного ответа может не быть: суммы 22 у чисел до 10 нет, и честная спецификация говорит, что тогда вернуть.
Граница правила «уточни вид ответа»: иногда заказчик сам не знает, что ему нужно, пока не увидит число. Тогда начинают с самого дешёвого вида - «есть ли» или «сколько» - и только потом решают, нужен ли список.
Что ломается без этого
Оптимизируешь не ту задачу. Неделя на быстрый подсчёт пар, а нужен был список - или наоборот.
Ответ не помещается. «Все пары похожих документов» на миллионе документов - это до 500 миллиардов пар. Узнать это до написания кода стоит одной строки арифметики.
Тихое расхождение на краях. Касающиеся интервалы, равные времена, пустой вход: тесты проходят, а в бою отчёт расходится с соседним отделом, который считал по-другому.
Зачем это в работе
Тикет «сделать отчёт по активным клиентам» через месяц превращается в спор, что значит «активный»: был вход за 30 дней, была покупка за 30 дней или за календарный месяц. Пять вопросов из этого урока, заданные в первый день, превращают спор в строчку спецификации. На собеседовании то же самое: Скиена прямо советует задавать уточняющие вопросы, потому что за верное решение неверно понятой задачи очков не дают.
Теперь сам
- Просьба: «найди самый популярный товар за неделю». Назови три вопроса, без ответа на которые программу писать нельзя.
- В списке из 10⁶ чисел все числа равны 5,
S = 10. Сколько пар с суммой 10? Можно ли вывести их все? - Для журнала из урока: K = 3, W = 60. Кто нарушитель по календарным часам, кто - по скользящему окну?
Ответ. 1. Что такое «популярный» (продажи в штуках, выручка, просмотры); что такое «неделя» (календарная или последние 7 дней, в чьём часовом поясе); что вернуть при равенстве (один товар или все с максимумом).
-
10⁶ · (10⁶ − 1) / 2 ≈ 5 · 10¹¹пар. Держать их списком нельзя: даже если пара - два номера по 4 байта, это 4 ТБ, а в Python каждая пара - отдельный список, и выходит в разы больше. Выводить потоком можно, но это5 · 10¹¹строк - даже при десяти миллионах строк в секунду около 14 часов. Свою скорость вывода легко измерить: засеки запись миллиона строк в файл. Число пар - одной формулой. -
По часам: у Боба в час 0 четыре входа (10, 55, 57, 58) - больше трёх, нарушитель; у Анны тоже четыре (5, 20, 40, 59) - нарушитель. Скользящим окном: оба, Боб - с минуты 10, Анна - с минуты 5.
Главное
Просьба становится задачей, когда записаны вход, вид ответа, критерий «лучше» и размер. Одна фраза при разных ответах - разные задачи с разной ценой, и выбрать толкование надо явно, до алгоритма.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий