ОП.04 · Тема 05Практикум · 45 минут
Страница 2 из 2две задачи · работа в парах

Практикум: выбор решения под ограничения

Одна задача — десять рабочих решений. Спрашиваем ограничения и выбираем то, которое под них подходит.

Что делаем в этом модуле

На лекции разбирали, как найти ошибку: уточнить условие, подобрать тесты, пройти код вручную. Здесь ошибок нет. В архиве две задачи, и к каждой лежит файл с десятью решениями. Все десять дают правильный ответ, пока соблюдены их требования к входу — это проверяется запуском, и проверка приложена.

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

Что в итоге получится

Заполненный бланк на две задачи: пять вопросов к условию, карта из десяти вариантов со временем и памятью, выбранный вариант под каждый из пяти заказов с объяснением и список вариантов, которые не подойдут никому.

Порядок работы

  1. Прочитайте условие задачи и запишите не меньше пяти вопросов, ответы на которые меняют выбор решения. Файл с вариантами пока не открывайте: вопросы задают до того, как увидели код.
  2. Откройте файл задачи и заполните карту вариантов: время, дополнительная память, требования к входу. Всё это есть в докстроке каждой функции — читать нужно именно её, а не угадывать по названию.
  3. Разберите заказы. Заказ — это набор ограничений: размер входа, диапазон значений, можно ли менять список, сколько памяти. Под каждый выберите один вариант и запишите, почему он, а не соседний по таблице.
  4. Назовите варианты, которые не подошли ни одному заказу, и условия, при которых они были бы уместны. Таких в каждой задаче два-три.
  5. Запустите проверку — она подтвердит, что дело не в правильности, а в ограничениях.
  6. Сверьтесь с парой и разберите расхождения в выборе.

Работают парами: участник 1 берёт задачу A, участник 2 — задачу B. Через десять минут меняются задачами, ещё три минуты — на сверку выбора. Разбор на странице открывается через минуту после того, как блок появился на экране: это время на собственный ответ.

Задача A. k-й по величине элемент

Условие дают в таком виде, и больше в нём ничего нет:

«Верните k-й по величине элемент списка целых чисел».

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

Попробуйте самостоятельно

Вопросы к условию задачи A

Запишите в бланк не меньше пяти вопросов, ответы на которые меняют выбор решения. Файл zadacha_a_kth.py пока не открывайте.

Открыть разбор
  • Насколько большим бывает список и насколько большим бывает k?
  • Повторы считаются? Для [10, 10, 5] и k = 2 ответ 10 или 5.
  • Можно ли менять исходный список или он нужен дальше в прежнем порядке?
  • Какие бывают значения: только целые неотрицательные или любые?
  • Сколько памяти можно занять сверх самого списка?
  • Гарантируется ли, что 1 ≤ k ≤ длины списка, и что делать, если нет?

Теперь файл. В нём десять функций с одинаковой сигнатурой (numbers, k); ниже три из них, чтобы было видно, насколько они разные:

zadacha_a_kth.py · три варианта из десятиСкачать файл ↓
def v01_sort_kopii(numbers, k):
    """Сортировка копии по убыванию. O(n log n) времени, O(n) памяти."""
    return sorted(numbers, reverse=True)[k - 1]


def v04_kucha_iz_k(numbers, k):
    """Своя куча из k наибольших. O(n log k) времени, O(k) памяти.
    Подходит, когда весь список сразу в память не помещается."""
    heap = []
    for value in numbers:
        if len(heap) < k:
            heapq.heappush(heap, value)
        elif value > heap[0]:
            heapq.heapreplace(heap, value)
    return heap[0]


def v09_dve_peremennye(numbers, k):
    """Один проход с двумя переменными. O(n) времени, O(1) памяти.
    Работает только при k = 2: для другого k нужен другой код."""
    if k != 2:
        raise ValueError('вариант v09 написан только для k = 2')

Первый вариант не требует от входа ничего и тратит O(n) памяти на копию. Второй экономит память, но платит логарифмом от k на каждом элементе. Третий быстрее обоих и при этом бесполезен, если k не равно двум. Такую же строку нужно написать про каждый из десяти вариантов — в файле для этого есть докстроки.

ВариантЧто делает
v01_sort_kopiiсортирует копию по убыванию и берёт элемент по индексу
v02_sort_na_mesteсортирует сам переданный список
v03_nlargestберёт k наибольших готовой функцией heapq.nlargest
v04_kucha_iz_kдержит кучу из k наибольших вручную
v05_k_prohodovk раз ищет максимум и выбрасывает его из копии
v06_quickselectбыстрый выбор с опорным элементом
v07_schetchikсчитает, сколько раз встретилось каждое значение
v08_bisect_top_kдержит отсортированный список из k наибольших
v09_dve_peremennyeодин проход с двумя переменными
v10_razlichnyeсначала убирает повторы, потом берёт k-е значение

Заказы

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

№Ограничения заказа
A1800 оценок, k любое, список нужен дальше в исходном порядке, память не ограничена
A210 000 000 чисел читаются по одному, нужны 10 наибольших, памяти мало
A3баллы теста — целые от 0 до 100, n = 1 000 000, ответ нужен быстро
A4n = 200 000, дополнительной памяти почти нет, исходный порядок дальше не нужен
A5нужна вторая по величине зарплата, причём одинаковые зарплаты считаются одной
Попробуйте самостоятельно

Выбор варианта под каждый заказ A

Заполните в бланке колонки «вариант» и «почему он». Затем назовите варианты, которые не подошли ни одному заказу.

Открыть разбор
№ВариантПочему он
A1v01_sort_kopii800 элементов сортируются мгновенно, а копия сохраняет исходный порядок. Памяти не жалко, поэтому усложнять нечем.
A2v04_kucha_iz_kЧисла приходят по одному, и держать все 10 000 000 негде. Куча из десяти элементов занимает O(k) памяти и обновляется на каждом числе.
A3v07_schetchikЗначений всего 101, поэтому массив счётчиков даёт ответ за один проход: O(n + m) вместо O(n log n) у сортировки.
A4v02_sort_na_mesteИсходный порядок не нужен, значит сортировку можно делать на месте и не тратить O(n) памяти на копию.
A5v10_razlichnye«Одинаковые считаются одной» — это другой контракт: k-е различное значение. Остальные девять вариантов ответят на другой вопрос.

Не подошли ни одному заказу. v05_k_prohodov и v08_bisect_top_k стоят O(n · k): они уместны только при очень маленьком k, и даже тогда v03 и v04 дешевле. v06_quickselect в среднем O(n), но в худшем случае O(n²), а выигрыш перед v03 в этих заказах не нужен. v09_dve_peremennye работает только при k = 2 — он подошёл бы к заказу A2, если бы там просили не десять наибольших, а второе по величине число.

Проверьте себя ещё раз: к заказу A3 подошёл бы и v01, он тоже вернёт правильный ответ. Вопрос в цене: сортировка миллиона чисел — это O(n log n), а счётчик по 101 значению — один проход.

Задача B. Есть ли значение в списке

Вторая задача устроена так же: условие без ограничений и десять рабочих решений в файле zadacha_b_poisk.py.

«Отвечайте на запросы, есть ли число x в списке».

Здесь появляется ограничение, которого не было в задаче A: запросов может быть много. Один поиск и двести тысяч поисков по одному и тому же списку — разные задачи, хотя условие у них одно. Функция получает список и список запросов, а возвращает список ответов True/False.

Попробуйте самостоятельно

Вопросы к условию задачи B

Запишите в бланк не меньше пяти вопросов. Один из них важнее остальных — попробуйте понять, какой.

Открыть разбор
  • Сколько будет запросов? Главный вопрос: подготовка данных окупается, только если запросов много.
  • Отсортирован ли список по условию?
  • Какие бывают значения: узкий диапазон вроде номеров аудиторий или любые целые?
  • Можно ли менять исходный список?
  • Сколько памяти можно занять сверх списка?
  • Нужно ли знать, сколько раз встречается значение, или только «есть ли»?
zadacha_b_poisk.py · два варианта из десятиСкачать файл ↓
def v03_set_zaranee(numbers, zaprosy):
    """Множество строится один раз до запросов.
    O(n + q) времени в среднем, O(n) памяти."""
    vidennye = set(numbers)
    return [x in vidennye for x in zaprosy]


def v08_set_kazhdyy_raz(numbers, zaprosy):
    """Множество строится внутри каждого запроса. Проверка быстрая,
    но построение стоит O(n), поэтому всего снова O(n * q)."""
    return [x in set(numbers) for x in zaprosy]

Эти две функции отличаются одним словом в строке, и обе используют set. Но первая строит множество один раз, а вторая — на каждый запрос, и вся выгода пропадает. Такие пары и есть то, ради чего читают код целиком, а не по названиям.

ВариантЧто делает
v01_lineyno_kazhdyyна каждый запрос свой проход по списку
v02_operator_inто же самое через оператор in
v03_set_zaraneeстроит множество один раз до запросов
v04_sort_kopii_bisectсортирует копию и ищет двоичным поиском
v05_sort_na_meste_bisectсортирует сам список и ищет двоичным поиском
v06_flagiдержит массив флагов по значениям
v07_slovar_kolichestvсчитает, сколько раз встретилось каждое значение
v08_set_kazhdyy_razстроит множество внутри каждого запроса
v09_binarnyy_poisk_svoyдвоичный поиск без сортировки
v10_sliyanie_zaprosovсортирует и список, и запросы, проходит по обоим один раз

Заказы

№Ограничения заказа
B1один запрос к списку из 100 000 чисел, порядок произвольный
B2200 000 запросов к списку из 1 000 000 чисел, памяти достаточно
B3список уже отсортирован по условию, 50 000 запросов, лишней памяти нет
B4значения — номера аудиторий от 0 до 999, запросов много
B5нужно знать не только «есть ли», но и сколько раз встречается
Попробуйте самостоятельно

Выбор варианта под каждый заказ B

Заполните бланк и назовите варианты, которые не подошли ни одному заказу.

Открыть разбор
№ВариантПочему он
B1v01_lineyno_kazhdyy или v02_operator_inОдин проход стоит O(n). Готовить данные ради единственного поиска невыгодно: сортировка или множество сами обойдутся дороже прохода.
B2v03_set_zaraneeМножество строится один раз за O(n), дальше каждый из 200 000 запросов стоит O(1) в среднем. Плата — O(n) памяти, и она разрешена.
B3v09_binarnyy_poisk_svoyСписок уже отсортирован, значит сортировать не нужно, а двоичный поиск даёт O(log n) на запрос и не требует памяти.
B4v06_flagiЗначений всего тысяча, поэтому массив флагов занимает O(m) и отвечает за O(1) без хеширования.
B5v07_slovar_kolichestvСловарь «значение → сколько раз» отвечает и на «есть ли», и на «сколько». Множество второй ответ дать не может.

Не подошли ни одному заказу. v08_set_kazhdyy_raz выглядит как B2, но строит множество заново на каждый запрос и оказывается медленнее обычного прохода: он полезен только как пример ошибки. v05_sort_na_meste_bisect портит исходный список, и ни один заказ этого не разрешает. v04_sort_kopii_bisect и v10_sliyanie_zaprosov рабочие, но в каждом заказе проигрывают: где памяти хватает, дешевле множество, а где список уже отсортирован — не нужна и сортировка.

Проверьте себя ещё раз: в заказе B2 множество выгоднее сортировки с двоичным поиском, потому что 200 000 запросов по O(1) дешевле 200 000 запросов по O(log n), а подготовка у множества тоже дешевле.

Проверка запуском

Выбор варианта — это утверждение о цене, а не о правильности. Чтобы это было видно, в архиве лежит проверка: она прогоняет все варианты на общих входах и сверяет ответы с эталоном, а для вариантов с особыми требованиями проверяет ещё и эти требования. Запускают её из папки praktikum:

Вывод в терминал$ python3 -m unittest -v test_variantov.py
test_obshchiy_kontrakt (test_variantov.ZadachaA.test_obshchiy_kontrakt)
Восемь вариантов работают на любом допустимом входе. ... ok
test_v02_menyaet_vhod (test_variantov.ZadachaA.test_v02_menyaet_vhod)
v02 сортирует переданный список: это его цена за O(1) памяти. ... ok
test_v07_trebuet_neotricatelnyh (test_variantov.ZadachaA.test_v07_trebuet_neotricatelnyh)
v07 строит массив по значениям, поэтому отрицательные ему не подходят. ... ok
test_v09_tolko_k2 (test_variantov.ZadachaA.test_v09_tolko_k2)
v09 отвечает верно при k = 2 и отказывается при другом k. ... ok
test_v10_drugoy_kontrakt (test_variantov.ZadachaA.test_v10_drugoy_kontrakt)
v10 отвечает на другой вопрос: k-е различное значение. ... ok
test_obshchiy_kontrakt (test_variantov.ZadachaB.test_obshchiy_kontrakt)
Восемь вариантов работают на любом списке целых чисел. ... ok
test_v05_menyaet_vhod (test_variantov.ZadachaB.test_v05_menyaet_vhod)
v05 сортирует переданный список, v04 работает с копией. ... ok
test_v06_trebuet_neotricatelnyh (test_variantov.ZadachaB.test_v06_trebuet_neotricatelnyh)
v06 держит флаги по значениям: нужен непустой список от 0 до m. ... ok
test_v09_trebuet_sortirovki (test_variantov.ZadachaB.test_v09_trebuet_sortirovki)
v09 верен на отсортированном входе и ошибается на неотсортированном. ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

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

Если хочется увидеть разницу во времени своими числами, в архиве есть zamer.py: он берёт несколько вариантов, прогоняет их на списках разного размера и печатает медиану трёх запусков, как в теме 4. Своих чисел здесь не приводим — их печатает ваш компьютер.

Что сдать

Работа сдаётся одним бланком на пару. В нём должно быть заполнено:

  • по пять вопросов к условию каждой задачи;
  • карта из десяти вариантов для своей задачи: время, дополнительная память, требования к входу;
  • выбранный вариант под каждый из пяти заказов и строка «почему он, а не соседний»;
  • варианты, не подошедшие ни одному заказу, и условия, при которых они были бы уместны;
  • итог запуска проверки и, если запускали, результат замера;
  • расхождения с парой после обмена задачами.

Бланк есть в архиве в виде blank.md и в PDF для печати — там же напечатан код всех двадцати вариантов, чтобы заполнять карту можно было без экрана.

Готовую работу кладут в репозиторий в папку lesson_05: заполненный бланк, файл vybor.py с выбранными функциями и номером заказа в комментарии над каждой, и файл с выводом проверки.

Как оценивается

КритерийГотово, если…Баллы
Вопросы к условиюНа каждую задачу записано не меньше пяти вопросов, и для каждого сказано, что меняет ответ.2
Карта вариантовДля всех десяти вариантов своей задачи заполнены время, дополнительная память и требования к входу.2
Выбор под заказыПод каждый из пяти заказов выбран вариант и объяснено, почему он, а не соседний по таблице.2
Отсеянные вариантыНазваны варианты, не подошедшие ни одному заказу, и условия, при которых они были бы уместны.2
Проверка и сверкаПроверка запущена и её вывод сохранён; задачи в паре обменяны, расхождения в выборе разобраны.2

Всего 10 баллов. Баллы даются за то, что выбор объяснён ограничениями, а не за совпадение с ключом. Шкалу перевода в отметку задаёт преподаватель.