Задание 26
Обработка данных с сортировкой
Записи и рейтинг, жадный выбор при ограниченной сумме, цепочки и пары, ряды и серии, интервалы времени и моделирование процесса по правилам.
7 подтем · 59 задач + 13 дополнительных · высокий · ≈ 35 мин
Подтема 1 из 7
Записи, сортировка и рейтинг
Что спрашивают в задании 26, как прочитать файл, хранить объект записью и сортировать по ключу; рейтинг и граница прохода.
Как устроена тема
В задании 26 дают файл с данными о сотнях объектов — файлах, товарах, заявках — и правила, по которым их выбирают или обслуживают. Вот задача, которую мы решим в этой теме:
«Входной файл 26-12.txt содержит заявки пассажиров, которые сдают багаж в камеру хранения. В заявке указаны время сдачи багажа и время освобождения ячейки (в минутах от начала суток). Ячейки пронумерованы начиная с 1. Багаж кладут в свободную ячейку с наименьшим номером; освободившуюся ячейку можно занять, начиная со следующей минуты после её освобождения. Если свободных ячеек нет, пассажир уходит. В первой строке файла — количество ячеек K, во второй — количество пассажиров N, в каждой из следующих N строк — время сдачи и время освобождения. Определите, сколько пассажиров сдадут багаж, и номер ячейки, в которую положит багаж последний из них.»
Почти всегда решение — сортировка записей и один проход по ним: либо ты сам выбираешь лучшее, либо в точности исполняешь правила условия. Разберём, что значат слова условия:
| В условии | Что это значит |
|---|---|
| заявка, строка файла | один объект — запись из нескольких чисел: (сдача, освобождение) |
| в минутах от начала суток | время — целое число от 0 до 1440, его можно сравнивать и вычитать |
| свободная ячейка с наименьшим номером | просматривать ячейки по порядку номеров и брать первую свободную |
| можно занять со следующей минуты | ячейка свободна, если освободилась строго раньше прихода: free_at < start |
| если свободных ячеек нет, пассажир уходит | заявку пропускают — ждать нельзя |
| максимальное количество, наибольший | выбираем лучший вариант — помогают сортировка и жадный выбор |
| если таких несколько, … | второй критерий: его применяют только среди равных по первому |
Начнём с файла, записей и сортировки — на них держатся все задачи темы. Тема состоит из шести подтем и дополнительного урока — у каждой своя страница:
- Записи, сортировка и рейтинг — ты сейчас здесь: чтение файла, ключ сортировки, рейтинг.
- Жадный выбор — больше всего файлов на диске, скидки.
- Цепочки коробок и пары — матрёшка, грузы и контейнеры, детали двух видов.
- Ряды, серии и группы — свободные места в рядах, линии на экране.
- Интервалы и события — мероприятия в зале, простои, одновременная нагрузка.
- Моделирование процесса — камера хранения, очереди; решим задачу темы.
- Пары, окно и сложные цепочки — дополнительный урок для редких задач.
Один объект
Что в задаче считается одним объектом и как хранить его в программе?
Объект — то, о чём одна строка файла: файл, товар, заявка. Его характеристики хранят вместе, одной записью, и сортируют записи целиком. В задаче темы объект — заявка пассажира: время сдачи и время освобождения.
Выбери объект и посмотри, из каких полей состоит его запись:
- файл
- размер
- товар
- цена, тип
- мероприятие
- время начала, время окончания
- участник
- идентификатор, баллы, штраф
- занятое место
- номер ряда, номер места
Сведения об одном объекте будем называть записью. Например, meeting = (120, 180) — одна запись о мероприятии: оно началось на 120-й минуте и закончилось на 180-й. Здесь используется : несколько значений, объединённых в одну последовательность. Отдельные элементы кортежа нельзя заменить присваиванием — для неизменяемой записи это удобно.
Почему записи сортируют целиком? Если отдельно отсортировать начала мероприятий и отдельно окончания, могут получиться пары, которых в исходных данных вообще не было:
meetings = [(10, 50), (20, 30)]starts = sorted(m[0] for m in meetings)finishes = sorted(m[1] for m in meetings)print(*zip(starts, finishes))print(*sorted(meetings))Мероприятий (10, 30) и (20, 50) не было: раздельная сортировка их выдумала. Исключение — условие прямо говорит о двух независимых наборах. Например, массы грузов и вместимости контейнеров могут быть двумя отдельными списками, несмотря на то что записаны в одних строках; такую задачу разберём в подтеме 3.
Ограничения и цель
Как понять, что именно нужно сделать лучшим и что важнее, если критериев несколько?
Выписать ограничения, цель и порядок критериев: сначала решает первый критерий, второй — только среди равных по первому.
Эти три вещи определяют алгоритм. Ограничение — правило, которое нельзя нарушать, например: суммарный размер файлов ≤ вместимость диска. Цель — величина, которую нужно сделать как можно больше или как можно меньше: количество сохранённых файлов → max. Вариант, удовлетворяющий ограничениям, называется допустимым. Лучший допустимый вариант по указанному критерию называется оптимальным.
Рассмотрим условие:
Сохранить максимальное количество файлов. Среди таких вариантов найти максимально возможный размер самого большого сохранённого файла.
Здесь два последовательных требования: сначала максимизируем количество, затем, сохраняя это количество, максимизируем размер одного файла. Пусть существуют варианты:
| Вариант | Количество файлов | Самый большой файл |
|---|---|---|
| А | 6 | 30 |
| Б | 6 | 45 |
| В | 5 | 100 |
Лучший вариант — Б. Вариант В проигрывает по первому, более важному критерию, и его 100 уже ничего не решает.
Такое сравнение называют лексикографическим: сначала сравнивают первый показатель, при равенстве — второй, затем третий и так далее. Термин связан со сравнением слов в словаре: сначала первая буква, при равенстве — следующая. Python именно так, поэтому вариант можно записать парой «количество, размер»:
variants = [(6, 30), (6, 45), (5, 100)]print(max(variants))Для любой задачи полезно заранее ответить на семь вопросов:
- Что является объектом? Файл, товар, заявка…
- Какие у него характеристики? Размер, цена, время…
- Можно ли менять порядок — или порядок задан условием?
- Что нельзя нарушать? Вместимость, совместимость, сроки…
- Что оптимизируем первым? Количество, стоимость, длину…
- Что оптимизируем при равенстве? Размер, номер, остаток…
- Что именно выводим? Количество, значение, ID, позицию…
Порядок чисел в ответе и порядок критериев — разные вещи. В задаче могут сначала требовать максимального общего количества товаров, а в ответ просить количество товаров типа А и остаток денег.
Задачи бывают двух видов: в одних ты сам выбираешь лучший вариант, в других обязан выполнить описанную процедуру — например, принять пассажиров в порядке прихода, как в задаче темы. Первому виду посвящены подтемы 2–5, второму — подтема 6.
Чтение файла
Как прочитать файл задачи темы?
Строго по описанию формата: в 26-12.txt сначала два числа в отдельных строках — K и N, — потом N строк по два числа. Вот начало файла:
301000397 404378 408753 822…30 ячеек, 1000 пассажиров, первый сдаёт багаж в минуту 397 и забирает в 404. Читаем так же, строку за строкой:
with open('26-12.txt') as f: k = int(f.readline()) n = int(f.readline()) requests = [] for i in range(n): start, finish = map(int, f.readline().split()) requests.append((start, finish)) print(k, n, requests[:3]) открывает файл и сам закроет его после блока. читает очередную строку, и следующее чтение продолжает со следующей. делит строку '397 404' на части, превращает их в числа, а start, finish = ... раскладывает по двум переменным. Заявка сохраняется — одной записью.
Универсального заголовка у файлов задания 26 нет: где-то в первой строке два числа, где-то они в разных строках, где-то заголовка нет вовсе. Сначала читай описание формата — выбери вариант:
- вместимость и количество
S, n = map(int, f.readline().split())- количество и вместимость
n, S = map(int, f.readline().split())- два числа в разных строках
k = int(f.readline()), затемn = int(f.readline())- три числа в первой строке
n, m, k = map(int, f.readline().split())
Сортировка
Зачем сортировать записи и как задать, по какому полю?
Сортировка даёт порядок, на который опирается рассуждение: в отсортированном списке каждое следующее число не меньше предыдущего. Записи сравниваются по первому полю, при равенстве — по второму; другое поле задаёт ключ.
После список [19, 8, 22, 12, 15] становится [8, 12, 15, 19, 22]. Поэтому, например, если очередной файл уже не помещается в оставшееся место, все следующие, ещё более крупные файлы тоже не поместятся. Для убывания пишут a.sort(reverse=True); создаёт новый отсортированный список и не меняет a.
Записи-кортежи обычная сортировка сравнивает по очереди: сначала первые числа, вторые — только при равенстве первых. Если нужно сортировать по второму числу, задают — функцию, которая для каждой записи говорит, по какому значению её сравнивать. Короче всего это записать через :
records = [(3, 8), (1, 9), (3, 2)]print(*sorted(records))print(*sorted(records, key=lambda record: record[1]))Во второй строке записи упорядочены по второму числу: 2, 8, 9. Та же функция с именем выглядела бы так: def second_value(record): return record[1] и key=second_value.
Заявки задачи темы обрабатывают в порядке прихода — по времени сдачи. Это первое поле записи, поэтому хватает обычной сортировки:
requests.sort()print(requests[:3])Сколько работы делает сортировка
Сортировку N записей Python делает примерно за N · log₂ N сравнений, а перебор всех пар — это N(N − 1)/2 пар:
from math import log2 for n in (1000, 100_000): print(n, round(n * log2(n)), n * (n - 1) // 2)При N = 100 000 сортировка — 1,7 миллиона сравнений, все пары — почти пять миллиардов. Поэтому решения темы строятся на сортировке и одном-двух проходах, а не на переборе всех наборов.
Ключ рейтинга
Как перевести правила рейтинга — «больше баллов выше, при равенстве меньше штраф» — в сортировку?
Записать правила ключом-кортежем по порядку важности: поле «чем больше, тем выше» — с минусом, «чем меньше, тем выше» — как есть.
Пусть записи имеют вид (student_id, score, penalty), а правила такие:
- Больше баллов — выше.
- При равенстве меньше штраф — выше.
- При полном равенстве меньше ID — выше.
students = [ (31, 80, 12), (7, 95, 20), (19, 95, 12), (4, 95, 12)]students.sort(key=lambda x: (-x[1], x[2], x[0]))print(*[s[0] for s in students])Почему перед баллами минус? Потому что 95 > 80, но −95 < −80. Обычная сортировка по возрастанию поставит -95 раньше -80, значит, участник с 95 баллами окажется раньше участника с 80. Почему 4 раньше 19? Баллы и штраф совпадают, решает третий показатель.
reverse=True переворачивает порядок всего ключа. Если направления полей различаются, меняют знак у конкретных числовых показателей. С key=lambda x: (x[1], -x[2], -x[0]) и reverse=True получился бы тот же порядок, но такую запись легко перепутать; ключ с минусами читается как сами правила.
Нужно различать три числа:
- ID участника — значение из файла;
- индекс в списке — позицию, с которой работает Python;
- место в рейтинге — обычно индекс плюс один.
students[2][0] — это ID участника на третьем месте, здесь 7.
Вычисляемые показатели
А если рейтинг строится по показателям, которых нет в файле, — сумме, среднему, числу двоек?
Сначала вычислить показатели для каждого участника, сложить их в запись вместе с ID и уже её сортировать.
Пусть за каждый вопрос викторины участник получил баллы: положительные за верный ответ, отрицательные за неверный, ноль — если не отвечал. Для одного участника:
scores = [5, -2, 0, 3, -1]total = 0positive = 0answered = 0for score in scores: total += score if score > 0: positive += score if score != 0: answered += 1print(total, positive, answered)Здесь total — сумма всех баллов, positive — сумма положительных баллов, answered — количество ненулевых ответов. После этого все показатели сохраняют вместе с ID в одну запись и сортируют записи.
Иногда показатели делят участников на группы. Например, в рейтинге сессии сначала идут студенты без двоек по убыванию среднего балла, затем студенты с двойками по возрастанию числа двоек, а внутри групп — по возрастанию ID. Среднее четырёх оценок не нужно делить: при одинаковом числе экзаменов больше среднее — больше сумма. Ключ записывает обе группы сразу:
marks = [ (10, 4, 4, 4, 5), (3, 5, 5, 4, 2), (6, 5, 4, 4, 4), (1, 2, 2, 3, 3), (8, 5, 5, 5, 4), (2, 3, 2, 4, 5),] def rating_key(r): twos = r[1:].count(2) if twos == 0: return (0, -sum(r[1:]), r[0]) return (twos, 0, r[0]) print(*[r[0] for r in sorted(marks, key=rating_key)])Первый элемент ключа — число двоек: у сдавших он 0, поэтому они впереди. Второй — сумма с минусом, только для сдавших; у несдавших там 0, и при равном числе двоек решает ID. Студенты 6 и 10 набрали по 17, поэтому выше тот, у кого меньше ID.
Граница прохода
Кто проходит, если «проходят первые 30 %», а у следующего участника те же баллы, что у последнего прошедшего?
Решает точная формулировка: доля — это целая часть, а «и все с такими же показателями» сдвигает границу дальше, пока показатели совпадают.
Две разные формулировки:
Проходят первые 25 участников рейтинга.
и:
Проходят все, набравшие не меньше баллов, чем участник на 25-м месте.
Во втором случае пройти может больше 25 человек. Если граница задана долей — «первые 30 % мест», — её считают целой частью: k = n * 30 // 100. Затем, если условие добавляет «а также все с такими же показателями, как у последнего из них», границу сдвигают дальше, пока показатели совпадают. В таблице ниже записи — пары «сумма, количество ответов», уже упорядоченные:
table = [(90, 6), (85, 7), (80, 5), (80, 5), (80, 6), (75, 4), (70, 8), (70, 9), (60, 5), (55, 7)]k = len(table) * 30 // 100last = table[k - 1]while k < len(table) and table[k] == last: k += 1print(len(table) * 30 // 100, k)Первые 30 % — это 3 участника, последний из них — (80, 5). Четвёртый с теми же показателями тоже проходит, а (80, 6) на пятом месте — нет: сумма та же, но число ответов другое. Равенство для прохода иногда определяется только баллами, хотя сама таблица дополнительно упорядочена по ID. Какие показатели должны совпасть, решает условие, и смешивать эти правила нельзя.
Похожее правило — проходной и полупроходной балл. Мест S; группы с одинаковой суммой баллов берут целиком сверху, пока группа помещается в оставшиеся места. Сумма последней целиком поместившейся группы — проходной балл. Следующая сумма, группа которой уже не помещается целиком, — полупроходной балл: из этой группы оставшиеся места делят по дополнительным показателям, например по баллу за математику.
Пусть мест 4, а суммы — 250, 240, 240, 235, 235, 235: группы 250 и 240 занимают три места, проходной балл 240; трое с 235 на одно место целиком не помещаются, и 235 — полупроходной балл.
Итого
- Записьполя одного объекта — вместе, сортируют целиком
- Чтение файластрого по описанию формата
- Критериивторой решает только среди равных по первому
- Ключ
key=lambda x: (-x[1], x[2], x[0]) - Показателисначала вычислить, потом сортировать
- Граница проходадоля — целая часть; равные последнему
Проверьте себя
Ответь сначала сам, потом открой ответ.
print(*sorted([(2, 5), (1, 7), (2, 3)]))?ОтветСкрыть ответ
(1, 7) (2, 3) (2, 5): при равных первых числах решает второе.(id, score, time). Правила: больше баллов — выше; при равенстве меньше время — выше; при полном равенстве больше ID — выше. Запиши ключ сортировки.ОтветСкрыть ответ
key=lambda x: (-x[1], x[2], -x[0]): минус у баллов и у ID — по убыванию, время — по возрастанию.ОтветСкрыть ответ
ОтветСкрыть ответ
ОтветСкрыть ответ
free_at < start, а 404 < 404 — ложь.Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Лучший вариант
Варианты заданы парами «количество, самый большой элемент»: (5, 40), (6, 12), (6, 15), (4, 100). Какой вариант лучший, если сначала важно количество, а затем самый большой элемент?
Введите два числа лучшего варианта через пробел.
Подсказка
Кортежи сравниваются по первому числу, при равенстве — по второму.
Решение
(6, 15): количество 6 у двух вариантов, при равенстве больше 15.
Решение программой
print(*max([(5, 40), (6, 12), (6, 15), (4, 100)]))Ответ: 6 15
Порядок по трём полям
Записи имеют вид «ID баллы штраф»: 12 80 5, 3 90 8, 7 90 4, 2 90 4. Порядок: баллы убывают, штраф возрастает, ID возрастает. Запиши порядок ID.
Введите ID через пробел в порядке рейтинга.
Подсказка
Минус в ключе даёт убывание: (−баллы, штраф, ID).
Решение
2 7 3 12: у первых двух равны баллы и штраф, решает ID.
Решение программой
records = [(12, 80, 5), (3, 90, 8), (7, 90, 4), (2, 90, 4)]records.sort(key=lambda r: (-r[1], r[2], r[0]))print(*[r[0] for r in records])Ответ: 2 7 3 12
Повышенная стипендия
В файле 26-1.txt записаны результаты сессии: в первой строке — количество студентов N, в каждой следующей — ID студента и четыре оценки от 2 до 5. Рейтинг составляют так: сначала студенты без двоек по убыванию суммы оценок, при равной сумме — по возрастанию ID; затем студенты с двойками по возрастанию количества двоек, при равном количестве — по возрастанию ID. Повышенную стипендию получают студенты, занявшие первые 15 % мест рейтинга (целая часть от 0,15 · N); гарантируется, что у всех них нет двоек. Запиши ID последнего студента с повышенной стипендией, затем количество студентов без двоек, сумма оценок которых равна сумме оценок этого студента.
Введите два числа через пробел в том порядке, в каком их спрашивают.
Подсказка
Ключ: (число двоек, −сумма у студентов без двоек, ID).
Решение
86976 440: ключ (число двоек, −сумма, ID); 15 % от 4000 — 600 мест.
Решение программой
with open('26-1.txt') as f: f.readline() data = [list(map(int, line.split())) for line in f] def key(r): twos = r[1:].count(2) if twos == 0: return (0, -sum(r[1:]), r[0]) return (twos, 0, r[0]) data.sort(key=key)k = len(data) * 15 // 100last = data[k - 1]same = 0for r in data: if 2 not in r[1:] and sum(r[1:]) == sum(last[1:]): same += 1print(last[0], same)Ответ: 86976 440
Победитель соревнования
Записи о решённых задачах (минута, участник): (14, 7), (3, 2), (25, 7), (9, 5), (31, 2), (12, 2), (40, 5), (18, 5), (44, 7), (52, 2), (36, 5), (61, 9). Побеждает тот, кто решил больше всех задач и первым набрал это число. Сколько задач решил победитель и какой у него номер?
Введите два числа через пробел в том порядке, в каком их спрашивают.
Подсказка
Иди по записям в порядке времени: считай задачи участника и запоминай минуту последней.
Решение
По 4 задачи у участников 2 и 5. Участник 5 набрал четвёртую в минуту 40, участник 2 — в 52. Победитель — 5.
Решение программой
solved = [(14, 7), (3, 2), (25, 7), (9, 5), (31, 2), (12, 2), (40, 5), (18, 5), (44, 7), (52, 2), (36, 5), (61, 9)]count = {}reached = {}for time, who in sorted(solved): count[who] = count.get(who, 0) + 1 reached[who] = timebest = max(count.values())winner = min((reached[w], w) for w in count if count[w] == best)print(best, winner[1])Ответ: 4 5
Сумма, положительные, ответы
Баллы участника за вопросы: 3, −2, 0, 5, −1, 0, 4. Найди сумму, сумму положительных и количество ответов.
Введите три числа через пробел в том порядке, в каком их спрашивают.
Подсказка
Ответ дан, если балл не равен нулю.
Решение
9 12 5: сумма 3 − 2 + 5 − 1 + 4, положительные 3 + 5 + 4, ненулевых баллов пять.
Решение программой
sc = [3, -2, 0, 5, -1, 0, 4]positive = sum(x for x in sc if x > 0)print(sum(sc), positive, sum(x != 0 for x in sc))Ответ: 9 12 5
Первые 25 % и равные последнему
Суммы баллов 12 участников в таблице по убыванию: 90, 85, 80, 80, 80, 75, 70, 70, 65, 60, 55, 50. Проходят первые 25 % мест. Сколько участников пройдёт, если условие добавляет «и все с такой же суммой, как у последнего из них», и сколько — без этой добавки?
Введите два числа через пробел: с добавкой, затем без неё.
Подсказка
Сначала целая часть 25 % от 12, затем все с суммой последнего прошедшего.
Решение
5 и 3: 25 % от 12 — три места, третий набрал 80, ещё двое с 80 проходят с добавкой.
Решение программой
a = [90, 85, 80, 80, 80, 75, 70, 70, 65, 60, 55, 50]k = len(a) * 25 // 100last = a[k - 1]print(sum(x >= last for x in a), k)Ответ: 5 3
Финалисты викторины
В файле 26-2.txt записаны результаты викторины: в первой строке — количество участников N, в каждой следующей — ID участника и баллы за 10 вопросов (положительный балл — верный ответ, отрицательный — неверный, 0 — ответа нет). Для каждого участника считают сумму баллов и количество данных ответов (ненулевых баллов). Таблица упорядочена по убыванию суммы, при равной сумме — по возрастанию количества ответов, при равенстве обоих показателей — по возрастанию ID. В финал проходят участники, занявшие первые 20 % мест таблицы (целая часть), а также все участники, у которых оба показателя такие же, как у последнего из них. Запиши количество финалистов, затем ID первого в таблице участника, не прошедшего в финал.
Введите два числа через пробел в том порядке, в каком их спрашивают.
Подсказка
После 20 % мест добавь всех с теми же двумя показателями, что у последнего.
Решение
615 1075: 20 % от 3000 — 600 мест, ещё 15 участников с теми же показателями, что у шестисотого.
Решение программой
with open('26-2.txt') as f: f.readline() data = [list(map(int, line.split())) for line in f] table = []for r in data: answers = sum(x != 0 for x in r[1:]) table.append((sum(r[1:]), answers, r[0]))table.sort(key=lambda t: (-t[0], t[1], t[2]))k = len(table) * 20 // 100last = table[k - 1]while k < len(table) and table[k][:2] == last[:2]: k += 1print(k, table[k][2])Ответ: 615 1075